nia-mcp-server 1.0.18__py3-none-any.whl → 1.0.20__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 +137 -8
- nia_mcp_server/server.py +1196 -403
- {nia_mcp_server-1.0.18.dist-info → nia_mcp_server-1.0.20.dist-info}/METADATA +1 -1
- {nia_mcp_server-1.0.18.dist-info → nia_mcp_server-1.0.20.dist-info}/RECORD +8 -8
- {nia_mcp_server-1.0.18.dist-info → nia_mcp_server-1.0.20.dist-info}/WHEEL +0 -0
- {nia_mcp_server-1.0.18.dist-info → nia_mcp_server-1.0.20.dist-info}/entry_points.txt +0 -0
- {nia_mcp_server-1.0.18.dist-info → nia_mcp_server-1.0.20.dist-info}/licenses/LICENSE +0 -0
nia_mcp_server/server.py
CHANGED
|
@@ -186,34 +186,22 @@ async def search_codebase(
|
|
|
186
186
|
try:
|
|
187
187
|
client = await ensure_api_client()
|
|
188
188
|
|
|
189
|
-
#
|
|
189
|
+
# Require explicit repository selection
|
|
190
190
|
if not repositories:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if repo_name:
|
|
206
|
-
repositories.append(repo_name)
|
|
207
|
-
else:
|
|
208
|
-
logger.warning(f"Repository missing 'repository' field: {repo}")
|
|
209
|
-
else:
|
|
210
|
-
logger.warning(f"Unexpected repository format: {type(repo)}, value: {repo}")
|
|
211
|
-
|
|
212
|
-
if not repositories:
|
|
213
|
-
return [TextContent(
|
|
214
|
-
type="text",
|
|
215
|
-
text="❌ No indexed repositories found. Use `index_repository` to index a codebase first."
|
|
216
|
-
)]
|
|
191
|
+
return [TextContent(
|
|
192
|
+
type="text",
|
|
193
|
+
text="🔍 **Please specify which repositories to search:**\n\n"
|
|
194
|
+
"1. Use `list_repositories` to see available repositories\n"
|
|
195
|
+
"2. Then call `search_codebase(\"your query\", [\"owner/repo1\", \"owner/repo2\"])`\n\n"
|
|
196
|
+
"**Example:**\n"
|
|
197
|
+
"```\n"
|
|
198
|
+
"search_codebase(\"How does auth work?\", [\"facebook/react\"])\n"
|
|
199
|
+
"```\n\n"
|
|
200
|
+
"**📌 Tip:** You can search specific folders using the exact format from `list_repositories`:\n"
|
|
201
|
+
"```\n"
|
|
202
|
+
"search_codebase(\"query\", [\"owner/repo/tree/branch/folder\"])\n"
|
|
203
|
+
"```"
|
|
204
|
+
)]
|
|
217
205
|
|
|
218
206
|
# Build messages for the query
|
|
219
207
|
messages = [
|
|
@@ -343,31 +331,46 @@ async def search_documentation(
|
|
|
343
331
|
include_sources: bool = True
|
|
344
332
|
) -> List[TextContent]:
|
|
345
333
|
"""
|
|
346
|
-
Search indexed documentation using natural language.
|
|
347
|
-
|
|
334
|
+
Search indexed documentation using natural language.
|
|
335
|
+
|
|
348
336
|
Args:
|
|
349
337
|
query: Natural language search query. Don't just use keywords or unstrctured query, make a comprehensive question to get the best results possible.
|
|
350
|
-
sources: List of documentation
|
|
338
|
+
sources: List of documentation identifiers to search. Can be:
|
|
339
|
+
- Display names (e.g., "Vercel AI SDK - Core")
|
|
340
|
+
- URLs (e.g., "https://sdk.vercel.ai/docs")
|
|
341
|
+
- Source IDs (UUID format for backwards compatibility)
|
|
351
342
|
include_sources: Whether to include source references in results
|
|
352
|
-
|
|
343
|
+
|
|
353
344
|
Returns:
|
|
354
345
|
Search results with relevant documentation excerpts
|
|
355
346
|
|
|
356
347
|
Important:
|
|
357
|
-
-
|
|
348
|
+
- You can now use friendly names instead of UUIDs! Try display names or URLs.
|
|
349
|
+
- If you don't know the identifiers, use `list_documentation` tool to see available options.
|
|
358
350
|
"""
|
|
359
351
|
try:
|
|
360
352
|
client = await ensure_api_client()
|
|
361
353
|
|
|
362
|
-
#
|
|
354
|
+
# Require explicit source selection
|
|
363
355
|
if not sources:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
356
|
+
return [TextContent(
|
|
357
|
+
type="text",
|
|
358
|
+
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\", [\"source1\", \"source2\"])`\n\n"
|
|
361
|
+
"**You can use any of these identifier formats:**\n"
|
|
362
|
+
"- Display names: `\"Vercel AI SDK - Core\"`\n"
|
|
363
|
+
"- URLs: `\"https://docs.trynia.ai/\"`\n"
|
|
364
|
+
"- UUIDs: `\"550e8400-e29b-41d4-a716-446655440000\"`\n\n"
|
|
365
|
+
"**Example:**\n"
|
|
366
|
+
"```\n"
|
|
367
|
+
"search_documentation(\"API reference\", [\"Vercel AI SDK - Core\"])\n"
|
|
368
|
+
"```\n\n"
|
|
369
|
+
"**📌 Tip:** Mix different identifier types in the same search:\n"
|
|
370
|
+
"```\n"
|
|
371
|
+
"search_documentation(\"query\", [\"Display Name\", \"https://docs.example.com/\"])\n"
|
|
372
|
+
"```"
|
|
373
|
+
)]
|
|
371
374
|
|
|
372
375
|
# Build messages for the query
|
|
373
376
|
messages = [
|
|
@@ -471,8 +474,8 @@ async def search_documentation(
|
|
|
471
474
|
text=f"❌ Error searching documentation: {str(e)}"
|
|
472
475
|
)]
|
|
473
476
|
|
|
474
|
-
@mcp.tool()
|
|
475
|
-
async def list_repositories() -> List[TextContent]:
|
|
477
|
+
# @mcp.tool()
|
|
478
|
+
# async def list_repositories() -> List[TextContent]:
|
|
476
479
|
"""
|
|
477
480
|
List all indexed repositories.
|
|
478
481
|
|
|
@@ -555,8 +558,8 @@ async def list_repositories() -> List[TextContent]:
|
|
|
555
558
|
text=f"❌ Error listing repositories: {error_msg}"
|
|
556
559
|
)]
|
|
557
560
|
|
|
558
|
-
@mcp.tool()
|
|
559
|
-
async def check_repository_status(repository: str) -> List[TextContent]:
|
|
561
|
+
# @mcp.tool()
|
|
562
|
+
# async def check_repository_status(repository: str) -> List[TextContent]:
|
|
560
563
|
"""
|
|
561
564
|
Check the indexing status of a repository.
|
|
562
565
|
|
|
@@ -704,8 +707,8 @@ async def index_documentation(
|
|
|
704
707
|
text=f"❌ Error indexing documentation: {str(e)}"
|
|
705
708
|
)]
|
|
706
709
|
|
|
707
|
-
@mcp.tool()
|
|
708
|
-
async def list_documentation() -> List[TextContent]:
|
|
710
|
+
# @mcp.tool()
|
|
711
|
+
# async def list_documentation() -> List[TextContent]:
|
|
709
712
|
"""
|
|
710
713
|
List all indexed documentation sources.
|
|
711
714
|
|
|
@@ -763,8 +766,8 @@ async def list_documentation() -> List[TextContent]:
|
|
|
763
766
|
text=f"❌ Error listing documentation: {str(e)}"
|
|
764
767
|
)]
|
|
765
768
|
|
|
766
|
-
@mcp.tool()
|
|
767
|
-
async def check_documentation_status(source_id: str) -> List[TextContent]:
|
|
769
|
+
# @mcp.tool()
|
|
770
|
+
# async def check_documentation_status(source_id: str) -> List[TextContent]:
|
|
768
771
|
"""
|
|
769
772
|
Check the indexing status of a documentation source.
|
|
770
773
|
|
|
@@ -829,8 +832,390 @@ async def check_documentation_status(source_id: str) -> List[TextContent]:
|
|
|
829
832
|
text=f"❌ Error checking documentation status: {str(e)}"
|
|
830
833
|
)]
|
|
831
834
|
|
|
835
|
+
# Combined Resource Management Tools
|
|
836
|
+
|
|
837
|
+
@mcp.tool()
|
|
838
|
+
async def rename_resource(
|
|
839
|
+
resource_type: str,
|
|
840
|
+
identifier: str,
|
|
841
|
+
new_name: str
|
|
842
|
+
) -> List[TextContent]:
|
|
843
|
+
"""
|
|
844
|
+
Rename a resource (repository or documentation) for better organization.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
resource_type: Type of resource - "repository" or "documentation"
|
|
848
|
+
identifier:
|
|
849
|
+
- For repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
850
|
+
- For documentation: Can be display name, URL, or UUID (e.g., "Vercel AI SDK - Core", "https://docs.trynia.ai/", or "doc-id-123")
|
|
851
|
+
new_name: New display name for the resource (1-100 characters)
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
Confirmation of rename operation
|
|
855
|
+
|
|
856
|
+
Examples:
|
|
857
|
+
- rename_resource("repository", "facebook/react", "React Framework")
|
|
858
|
+
- rename_resource("documentation", "Vercel AI SDK - Core", "Python Official Docs")
|
|
859
|
+
- rename_resource("documentation", "https://docs.trynia.ai/", "NIA Documentation")
|
|
860
|
+
"""
|
|
861
|
+
try:
|
|
862
|
+
# Validate resource type
|
|
863
|
+
if resource_type not in ["repository", "documentation"]:
|
|
864
|
+
return [TextContent(
|
|
865
|
+
type="text",
|
|
866
|
+
text=f"❌ Invalid resource_type: '{resource_type}'. Must be 'repository' or 'documentation'."
|
|
867
|
+
)]
|
|
868
|
+
|
|
869
|
+
# Validate name length
|
|
870
|
+
if not new_name or len(new_name) > 100:
|
|
871
|
+
return [TextContent(
|
|
872
|
+
type="text",
|
|
873
|
+
text="❌ Display name must be between 1 and 100 characters."
|
|
874
|
+
)]
|
|
875
|
+
|
|
876
|
+
client = await ensure_api_client()
|
|
877
|
+
|
|
878
|
+
if resource_type == "repository":
|
|
879
|
+
result = await client.rename_repository(identifier, new_name)
|
|
880
|
+
resource_desc = f"repository '{identifier}'"
|
|
881
|
+
else: # documentation
|
|
882
|
+
result = await client.rename_data_source(identifier, new_name)
|
|
883
|
+
resource_desc = f"documentation source"
|
|
884
|
+
|
|
885
|
+
if result.get("success"):
|
|
886
|
+
return [TextContent(
|
|
887
|
+
type="text",
|
|
888
|
+
text=f"✅ Successfully renamed {resource_desc} to '{new_name}'"
|
|
889
|
+
)]
|
|
890
|
+
else:
|
|
891
|
+
return [TextContent(
|
|
892
|
+
type="text",
|
|
893
|
+
text=f"❌ Failed to rename {resource_type}: {result.get('message', 'Unknown error')}"
|
|
894
|
+
)]
|
|
895
|
+
|
|
896
|
+
except APIError as e:
|
|
897
|
+
logger.error(f"API Error renaming {resource_type}: {e}")
|
|
898
|
+
error_msg = f"❌ {str(e)}"
|
|
899
|
+
if e.status_code == 403 and "lifetime limit" in str(e).lower():
|
|
900
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit. Upgrade to Pro for unlimited access."
|
|
901
|
+
return [TextContent(type="text", text=error_msg)]
|
|
902
|
+
except Exception as e:
|
|
903
|
+
logger.error(f"Error renaming {resource_type}: {e}")
|
|
904
|
+
return [TextContent(
|
|
905
|
+
type="text",
|
|
906
|
+
text=f"❌ Error renaming {resource_type}: {str(e)}"
|
|
907
|
+
)]
|
|
908
|
+
|
|
909
|
+
@mcp.tool()
|
|
910
|
+
async def delete_resource(
|
|
911
|
+
resource_type: str,
|
|
912
|
+
identifier: str
|
|
913
|
+
) -> List[TextContent]:
|
|
914
|
+
"""
|
|
915
|
+
Delete an indexed resource (repository or documentation).
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
resource_type: Type of resource - "repository" or "documentation"
|
|
919
|
+
identifier:
|
|
920
|
+
- For repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
921
|
+
- For documentation: Can be display name, URL, or UUID (e.g., "Vercel AI SDK - Core", "https://docs.trynia.ai/", or "doc-id-123")
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
Confirmation of deletion
|
|
925
|
+
|
|
926
|
+
Examples:
|
|
927
|
+
- delete_resource("repository", "facebook/react")
|
|
928
|
+
- delete_resource("documentation", "Vercel AI SDK - Core")
|
|
929
|
+
- delete_resource("documentation", "https://docs.trynia.ai/")
|
|
930
|
+
"""
|
|
931
|
+
try:
|
|
932
|
+
# Validate resource type
|
|
933
|
+
if resource_type not in ["repository", "documentation"]:
|
|
934
|
+
return [TextContent(
|
|
935
|
+
type="text",
|
|
936
|
+
text=f"❌ Invalid resource_type: '{resource_type}'. Must be 'repository' or 'documentation'."
|
|
937
|
+
)]
|
|
938
|
+
|
|
939
|
+
client = await ensure_api_client()
|
|
940
|
+
|
|
941
|
+
if resource_type == "repository":
|
|
942
|
+
success = await client.delete_repository(identifier)
|
|
943
|
+
resource_desc = f"repository: {identifier}"
|
|
944
|
+
else: # documentation
|
|
945
|
+
success = await client.delete_data_source(identifier)
|
|
946
|
+
resource_desc = f"documentation source: {identifier}"
|
|
947
|
+
|
|
948
|
+
if success:
|
|
949
|
+
return [TextContent(
|
|
950
|
+
type="text",
|
|
951
|
+
text=f"✅ Successfully deleted {resource_desc}"
|
|
952
|
+
)]
|
|
953
|
+
else:
|
|
954
|
+
return [TextContent(
|
|
955
|
+
type="text",
|
|
956
|
+
text=f"❌ Failed to delete {resource_desc}"
|
|
957
|
+
)]
|
|
958
|
+
|
|
959
|
+
except APIError as e:
|
|
960
|
+
logger.error(f"API Error deleting {resource_type}: {e}")
|
|
961
|
+
error_msg = f"❌ {str(e)}"
|
|
962
|
+
if e.status_code == 403 and "lifetime limit" in str(e).lower():
|
|
963
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit of 3 indexing operations. Upgrade to Pro for unlimited access."
|
|
964
|
+
return [TextContent(type="text", text=error_msg)]
|
|
965
|
+
except Exception as e:
|
|
966
|
+
logger.error(f"Error deleting {resource_type}: {e}")
|
|
967
|
+
return [TextContent(
|
|
968
|
+
type="text",
|
|
969
|
+
text=f"❌ Error deleting {resource_type}: {str(e)}"
|
|
970
|
+
)]
|
|
971
|
+
|
|
972
|
+
@mcp.tool()
|
|
973
|
+
async def check_resource_status(
|
|
974
|
+
resource_type: str,
|
|
975
|
+
identifier: str
|
|
976
|
+
) -> List[TextContent]:
|
|
977
|
+
"""
|
|
978
|
+
Check the indexing status of a resource (repository or documentation).
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
resource_type: Type of resource - "repository" or "documentation"
|
|
982
|
+
identifier:
|
|
983
|
+
- For repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
984
|
+
- For documentation: Source ID (UUID format only) - use list_resources to get the UUID
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
Current status of the resource
|
|
988
|
+
|
|
989
|
+
Examples:
|
|
990
|
+
- check_resource_status("repository", "facebook/react")
|
|
991
|
+
- check_resource_status("documentation", "550e8400-e29b-41d4-a716-446655440000")
|
|
992
|
+
|
|
993
|
+
Note:
|
|
994
|
+
- Documentation status checking requires UUID identifiers only
|
|
995
|
+
- Use list_resources("documentation") to find the UUID for a documentation source
|
|
996
|
+
"""
|
|
997
|
+
try:
|
|
998
|
+
# Validate resource type
|
|
999
|
+
if resource_type not in ["repository", "documentation"]:
|
|
1000
|
+
return [TextContent(
|
|
1001
|
+
type="text",
|
|
1002
|
+
text=f"❌ Invalid resource_type: '{resource_type}'. Must be 'repository' or 'documentation'."
|
|
1003
|
+
)]
|
|
1004
|
+
|
|
1005
|
+
client = await ensure_api_client()
|
|
1006
|
+
|
|
1007
|
+
if resource_type == "repository":
|
|
1008
|
+
status = await client.get_repository_status(identifier)
|
|
1009
|
+
if not status:
|
|
1010
|
+
return [TextContent(
|
|
1011
|
+
type="text",
|
|
1012
|
+
text=f"❌ Repository '{identifier}' not found."
|
|
1013
|
+
)]
|
|
1014
|
+
title = f"Repository Status: {identifier}"
|
|
1015
|
+
status_key = "status"
|
|
1016
|
+
else: # documentation
|
|
1017
|
+
status = await client.get_data_source_status(identifier)
|
|
1018
|
+
if not status:
|
|
1019
|
+
return [TextContent(
|
|
1020
|
+
type="text",
|
|
1021
|
+
text=f"❌ Documentation source '{identifier}' not found."
|
|
1022
|
+
)]
|
|
1023
|
+
title = f"Documentation Status: {status.get('url', 'Unknown URL')}"
|
|
1024
|
+
status_key = "status"
|
|
1025
|
+
|
|
1026
|
+
# Format status with appropriate icon
|
|
1027
|
+
status_text = status.get(status_key, "unknown")
|
|
1028
|
+
status_icon = {
|
|
1029
|
+
"completed": "✅",
|
|
1030
|
+
"indexing": "⏳",
|
|
1031
|
+
"processing": "⏳",
|
|
1032
|
+
"failed": "❌",
|
|
1033
|
+
"pending": "🔄",
|
|
1034
|
+
"error": "❌"
|
|
1035
|
+
}.get(status_text, "❓")
|
|
1036
|
+
|
|
1037
|
+
lines = [
|
|
1038
|
+
f"# {title}\n",
|
|
1039
|
+
f"{status_icon} **Status:** {status_text}"
|
|
1040
|
+
]
|
|
1041
|
+
|
|
1042
|
+
# Add resource-specific fields
|
|
1043
|
+
if resource_type == "repository":
|
|
1044
|
+
lines.append(f"**Branch:** {status.get('branch', 'main')}")
|
|
1045
|
+
if status.get("progress"):
|
|
1046
|
+
progress = status["progress"]
|
|
1047
|
+
if isinstance(progress, dict):
|
|
1048
|
+
lines.append(f"**Progress:** {progress.get('percentage', 0)}%")
|
|
1049
|
+
if progress.get("stage"):
|
|
1050
|
+
lines.append(f"**Stage:** {progress['stage']}")
|
|
1051
|
+
else: # documentation
|
|
1052
|
+
lines.append(f"**Source ID:** {identifier}")
|
|
1053
|
+
if status.get("page_count", 0) > 0:
|
|
1054
|
+
lines.append(f"**Pages Indexed:** {status['page_count']}")
|
|
1055
|
+
if status.get("details"):
|
|
1056
|
+
details = status["details"]
|
|
1057
|
+
if details.get("progress"):
|
|
1058
|
+
lines.append(f"**Progress:** {details['progress']}%")
|
|
1059
|
+
if details.get("stage"):
|
|
1060
|
+
lines.append(f"**Stage:** {details['stage']}")
|
|
1061
|
+
|
|
1062
|
+
# Common fields
|
|
1063
|
+
if status.get("indexed_at"):
|
|
1064
|
+
lines.append(f"**Indexed:** {status['indexed_at']}")
|
|
1065
|
+
elif status.get("created_at"):
|
|
1066
|
+
lines.append(f"**Created:** {status['created_at']}")
|
|
1067
|
+
|
|
1068
|
+
if status.get("error"):
|
|
1069
|
+
lines.append(f"**Error:** {status['error']}")
|
|
1070
|
+
|
|
1071
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
1072
|
+
|
|
1073
|
+
except APIError as e:
|
|
1074
|
+
logger.error(f"API Error checking {resource_type} status: {e}")
|
|
1075
|
+
error_msg = f"❌ {str(e)}"
|
|
1076
|
+
if e.status_code == 403 and "lifetime limit" in str(e).lower():
|
|
1077
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit of 3 indexing operations. Upgrade to Pro for unlimited access."
|
|
1078
|
+
return [TextContent(type="text", text=error_msg)]
|
|
1079
|
+
except Exception as e:
|
|
1080
|
+
logger.error(f"Error checking {resource_type} status: {e}")
|
|
1081
|
+
return [TextContent(
|
|
1082
|
+
type="text",
|
|
1083
|
+
text=f"❌ Error checking {resource_type} status: {str(e)}"
|
|
1084
|
+
)]
|
|
1085
|
+
|
|
832
1086
|
@mcp.tool()
|
|
833
|
-
async def
|
|
1087
|
+
async def list_resources(
|
|
1088
|
+
resource_type: Optional[str] = None
|
|
1089
|
+
) -> List[TextContent]:
|
|
1090
|
+
"""
|
|
1091
|
+
List indexed resources (repositories and/or documentation).
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
resource_type: Optional filter - "repository", "documentation", or None for all
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
List of indexed resources with their status
|
|
1098
|
+
|
|
1099
|
+
Examples:
|
|
1100
|
+
- list_resources() - List all resources
|
|
1101
|
+
- list_resources("repository") - List only repositories
|
|
1102
|
+
- list_resources("documentation") - List only documentation
|
|
1103
|
+
"""
|
|
1104
|
+
try:
|
|
1105
|
+
# Validate resource type if provided
|
|
1106
|
+
if resource_type and resource_type not in ["repository", "documentation"]:
|
|
1107
|
+
return [TextContent(
|
|
1108
|
+
type="text",
|
|
1109
|
+
text=f"❌ Invalid resource_type: '{resource_type}'. Must be 'repository', 'documentation', or None for all."
|
|
1110
|
+
)]
|
|
1111
|
+
|
|
1112
|
+
client = await ensure_api_client()
|
|
1113
|
+
lines = []
|
|
1114
|
+
|
|
1115
|
+
# Determine what to list
|
|
1116
|
+
list_repos = resource_type in [None, "repository"]
|
|
1117
|
+
list_docs = resource_type in [None, "documentation"]
|
|
1118
|
+
|
|
1119
|
+
if list_repos:
|
|
1120
|
+
repositories = await client.list_repositories()
|
|
1121
|
+
|
|
1122
|
+
if repositories:
|
|
1123
|
+
lines.append("# Indexed Repositories\n")
|
|
1124
|
+
for repo in repositories:
|
|
1125
|
+
status_icon = "✅" if repo.get("status") == "completed" else "⏳"
|
|
1126
|
+
|
|
1127
|
+
# Show display name if available, otherwise show repository
|
|
1128
|
+
display_name = repo.get("display_name")
|
|
1129
|
+
repo_name = repo['repository']
|
|
1130
|
+
|
|
1131
|
+
if display_name:
|
|
1132
|
+
lines.append(f"\n## {status_icon} {display_name}")
|
|
1133
|
+
lines.append(f"- **Repository:** {repo_name}")
|
|
1134
|
+
else:
|
|
1135
|
+
lines.append(f"\n## {status_icon} {repo_name}")
|
|
1136
|
+
|
|
1137
|
+
lines.append(f"- **Branch:** {repo.get('branch', 'main')}")
|
|
1138
|
+
lines.append(f"- **Status:** {repo.get('status', 'unknown')}")
|
|
1139
|
+
if repo.get("indexed_at"):
|
|
1140
|
+
lines.append(f"- **Indexed:** {repo['indexed_at']}")
|
|
1141
|
+
if repo.get("error"):
|
|
1142
|
+
lines.append(f"- **Error:** {repo['error']}")
|
|
1143
|
+
|
|
1144
|
+
# Add usage hint for completed repositories
|
|
1145
|
+
if repo.get("status") == "completed":
|
|
1146
|
+
lines.append(f"- **Usage:** `search_codebase(query, [\"{repo_name}\"])`")
|
|
1147
|
+
elif resource_type == "repository":
|
|
1148
|
+
lines.append("No indexed repositories found.\n\n")
|
|
1149
|
+
lines.append("Get started by indexing a repository:\n")
|
|
1150
|
+
lines.append("Use `index_repository` with a GitHub URL.")
|
|
1151
|
+
|
|
1152
|
+
if list_docs:
|
|
1153
|
+
sources = await client.list_data_sources()
|
|
1154
|
+
|
|
1155
|
+
if sources:
|
|
1156
|
+
if lines: # Add separator if we already have repositories
|
|
1157
|
+
lines.append("\n---\n")
|
|
1158
|
+
lines.append("# Indexed Documentation\n")
|
|
1159
|
+
|
|
1160
|
+
for source in sources:
|
|
1161
|
+
status_icon = "✅" if source.get("status") == "completed" else "⏳"
|
|
1162
|
+
|
|
1163
|
+
# Show display name if available, otherwise show URL
|
|
1164
|
+
display_name = source.get("display_name")
|
|
1165
|
+
url = source.get('url', 'Unknown URL')
|
|
1166
|
+
|
|
1167
|
+
if display_name:
|
|
1168
|
+
lines.append(f"\n## {status_icon} {display_name}")
|
|
1169
|
+
lines.append(f"- **URL:** {url}")
|
|
1170
|
+
else:
|
|
1171
|
+
lines.append(f"\n## {status_icon} {url}")
|
|
1172
|
+
|
|
1173
|
+
lines.append(f"- **ID:** {source['id']}")
|
|
1174
|
+
lines.append(f"- **Status:** {source.get('status', 'unknown')}")
|
|
1175
|
+
lines.append(f"- **Type:** {source.get('source_type', 'web')}")
|
|
1176
|
+
if source.get("page_count", 0) > 0:
|
|
1177
|
+
lines.append(f"- **Pages:** {source['page_count']}")
|
|
1178
|
+
if source.get("created_at"):
|
|
1179
|
+
lines.append(f"- **Created:** {source['created_at']}")
|
|
1180
|
+
elif resource_type == "documentation":
|
|
1181
|
+
lines.append("No indexed documentation found.\n\n")
|
|
1182
|
+
lines.append("Get started by indexing documentation:\n")
|
|
1183
|
+
lines.append("Use `index_documentation` with a URL.")
|
|
1184
|
+
|
|
1185
|
+
if not lines:
|
|
1186
|
+
lines.append("No indexed resources found.\n\n")
|
|
1187
|
+
lines.append("Get started by indexing:\n")
|
|
1188
|
+
lines.append("- Use `index_repository` for GitHub repositories\n")
|
|
1189
|
+
lines.append("- Use `index_documentation` for documentation sites")
|
|
1190
|
+
|
|
1191
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
1192
|
+
|
|
1193
|
+
except APIError as e:
|
|
1194
|
+
logger.error(f"API Error listing resources: {e}")
|
|
1195
|
+
error_msg = f"❌ {str(e)}"
|
|
1196
|
+
if e.status_code == 403 or "free tier limit" in str(e).lower():
|
|
1197
|
+
if e.detail and "3 free indexing operations" in e.detail:
|
|
1198
|
+
error_msg = f"❌ {e.detail}\n\n💡 Tip: Upgrade to Pro at https://trynia.ai/billing for unlimited indexing."
|
|
1199
|
+
else:
|
|
1200
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit. Upgrade to Pro for unlimited access."
|
|
1201
|
+
return [TextContent(type="text", text=error_msg)]
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
logger.error(f"Unexpected error listing resources: {e}")
|
|
1204
|
+
error_msg = str(e)
|
|
1205
|
+
if "indexing operations" in error_msg.lower() or "lifetime limit" in error_msg.lower():
|
|
1206
|
+
return [TextContent(
|
|
1207
|
+
type="text",
|
|
1208
|
+
text=f"❌ {error_msg}\n\n💡 Tip: Upgrade to Pro at https://trynia.ai/billing for unlimited indexing."
|
|
1209
|
+
)]
|
|
1210
|
+
return [TextContent(
|
|
1211
|
+
type="text",
|
|
1212
|
+
text=f"❌ Error listing resources: {error_msg}"
|
|
1213
|
+
)]
|
|
1214
|
+
|
|
1215
|
+
# Old individual tools (to be commented out after testing)
|
|
1216
|
+
|
|
1217
|
+
# @mcp.tool()
|
|
1218
|
+
# async def delete_documentation(source_id: str) -> List[TextContent]:
|
|
834
1219
|
"""
|
|
835
1220
|
Delete an indexed documentation source.
|
|
836
1221
|
|
|
@@ -868,8 +1253,8 @@ async def delete_documentation(source_id: str) -> List[TextContent]:
|
|
|
868
1253
|
text=f"❌ Error deleting documentation: {str(e)}"
|
|
869
1254
|
)]
|
|
870
1255
|
|
|
871
|
-
@mcp.tool()
|
|
872
|
-
async def delete_repository(repository: str) -> List[TextContent]:
|
|
1256
|
+
# @mcp.tool()
|
|
1257
|
+
# async def delete_repository(repository: str) -> List[TextContent]:
|
|
873
1258
|
"""
|
|
874
1259
|
Delete an indexed repository.
|
|
875
1260
|
|
|
@@ -907,8 +1292,8 @@ async def delete_repository(repository: str) -> List[TextContent]:
|
|
|
907
1292
|
text=f"❌ Error deleting repository: {str(e)}"
|
|
908
1293
|
)]
|
|
909
1294
|
|
|
910
|
-
@mcp.tool()
|
|
911
|
-
async def rename_repository(repository: str, new_name: str) -> List[TextContent]:
|
|
1295
|
+
# @mcp.tool()
|
|
1296
|
+
# async def rename_repository(repository: str, new_name: str) -> List[TextContent]:
|
|
912
1297
|
"""
|
|
913
1298
|
Rename an indexed repository for better organization.
|
|
914
1299
|
|
|
@@ -954,8 +1339,8 @@ async def rename_repository(repository: str, new_name: str) -> List[TextContent]
|
|
|
954
1339
|
text=f"❌ Error renaming repository: {str(e)}"
|
|
955
1340
|
)]
|
|
956
1341
|
|
|
957
|
-
@mcp.tool()
|
|
958
|
-
async def rename_documentation(source_id: str, new_name: str) -> List[TextContent]:
|
|
1342
|
+
# @mcp.tool()
|
|
1343
|
+
# async def rename_documentation(source_id: str, new_name: str) -> List[TextContent]:
|
|
959
1344
|
"""
|
|
960
1345
|
Rename a documentation source for better organization.
|
|
961
1346
|
|
|
@@ -1633,406 +2018,814 @@ async def read_source_content(
|
|
|
1633
2018
|
text=f"❌ Error reading source content: {str(e)}"
|
|
1634
2019
|
)]
|
|
1635
2020
|
|
|
2021
|
+
# @mcp.tool()
|
|
2022
|
+
# async def index_local_filesystem(
|
|
2023
|
+
# directory_path: str,
|
|
2024
|
+
# inclusion_patterns: Optional[List[str]] = None,
|
|
2025
|
+
# exclusion_patterns: Optional[List[str]] = None,
|
|
2026
|
+
# max_file_size_mb: int = 50
|
|
2027
|
+
# ) -> List[TextContent]:
|
|
2028
|
+
# """
|
|
2029
|
+
# Index a local filesystem directory for intelligent search.
|
|
2030
|
+
#
|
|
2031
|
+
# Args:
|
|
2032
|
+
# directory_path: Absolute path to the directory to index
|
|
2033
|
+
# inclusion_patterns: Optional list of patterns to include (e.g., ["ext:.py", "dir:src"])
|
|
2034
|
+
# exclusion_patterns: Optional list of patterns to exclude (e.g., ["dir:node_modules", "ext:.log"])
|
|
2035
|
+
# max_file_size_mb: Maximum file size in MB to process (default: 50)
|
|
2036
|
+
#
|
|
2037
|
+
# Returns:
|
|
2038
|
+
# Status of the indexing operation
|
|
2039
|
+
#
|
|
2040
|
+
# Important:
|
|
2041
|
+
# - Path must be absolute (e.g., /Users/username/projects/myproject)
|
|
2042
|
+
# - When indexing starts, use check_local_filesystem_status tool to monitor progress
|
|
2043
|
+
# """
|
|
2044
|
+
# try:
|
|
2045
|
+
# # Validate absolute path
|
|
2046
|
+
# if not os.path.isabs(directory_path):
|
|
2047
|
+
# return [TextContent(
|
|
2048
|
+
# type="text",
|
|
2049
|
+
# text=f"❌ Error: directory_path must be an absolute path. Got: {directory_path}\n\n"
|
|
2050
|
+
# f"Example: /Users/username/projects/myproject"
|
|
2051
|
+
# )]
|
|
2052
|
+
#
|
|
2053
|
+
# client = await ensure_api_client()
|
|
2054
|
+
#
|
|
2055
|
+
# # Start indexing
|
|
2056
|
+
# logger.info(f"Starting to index local directory: {directory_path}")
|
|
2057
|
+
# result = await client.index_local_filesystem(
|
|
2058
|
+
# directory_path=directory_path,
|
|
2059
|
+
# inclusion_patterns=inclusion_patterns or [],
|
|
2060
|
+
# exclusion_patterns=exclusion_patterns or [],
|
|
2061
|
+
# max_file_size_mb=max_file_size_mb
|
|
2062
|
+
# )
|
|
2063
|
+
#
|
|
2064
|
+
# if result.get("success"):
|
|
2065
|
+
# source_id = result["data"]["source_id"]
|
|
2066
|
+
# status_url = result["data"]["status_url"]
|
|
2067
|
+
#
|
|
2068
|
+
# return [TextContent(
|
|
2069
|
+
# type="text",
|
|
2070
|
+
# text=(
|
|
2071
|
+
# f"✅ Successfully started indexing local directory!\n\n"
|
|
2072
|
+
# f"📁 **Directory:** `{directory_path}`\n"
|
|
2073
|
+
# f"🆔 **Source ID:** `{source_id}`\n"
|
|
2074
|
+
# f"📊 **Status:** Processing\n\n"
|
|
2075
|
+
# f"**What happens next:**\n"
|
|
2076
|
+
# f"• NIA is scanning and indexing your files in the background\n"
|
|
2077
|
+
# f"• This process typically takes a few minutes depending on directory size\n"
|
|
2078
|
+
# f"• Use `check_local_filesystem_status` with source ID `{source_id}` to monitor progress\n"
|
|
2079
|
+
# f"• Once indexed, use `search_codebase` or `search_documentation` to search your files\n\n"
|
|
2080
|
+
# f"📌 **Tip:** You can check the status at any time or visit [app.trynia.ai](https://app.trynia.ai) to monitor progress."
|
|
2081
|
+
# )
|
|
2082
|
+
# )]
|
|
2083
|
+
# else:
|
|
2084
|
+
# return [TextContent(
|
|
2085
|
+
# type="text",
|
|
2086
|
+
# text=f"❌ Failed to start indexing: {result.get('detail', 'Unknown error')}"
|
|
2087
|
+
# )]
|
|
2088
|
+
#
|
|
2089
|
+
# except APIError as e:
|
|
2090
|
+
# logger.error(f"API error indexing local filesystem: {e}")
|
|
2091
|
+
# return [TextContent(
|
|
2092
|
+
# type="text",
|
|
2093
|
+
# text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
2094
|
+
# )]
|
|
2095
|
+
# except Exception as e:
|
|
2096
|
+
# logger.error(f"Unexpected error indexing local filesystem: {e}")
|
|
2097
|
+
# return [TextContent(
|
|
2098
|
+
# type="text",
|
|
2099
|
+
# text=f"❌ Error: An unexpected error occurred while indexing the directory: {str(e)}"
|
|
2100
|
+
# )]
|
|
2101
|
+
|
|
2102
|
+
# @mcp.tool()
|
|
2103
|
+
# async def scan_local_filesystem(
|
|
2104
|
+
# directory_path: str,
|
|
2105
|
+
# inclusion_patterns: Optional[List[str]] = None,
|
|
2106
|
+
# exclusion_patterns: Optional[List[str]] = None,
|
|
2107
|
+
# max_file_size_mb: int = 50
|
|
2108
|
+
# ) -> List[TextContent]:
|
|
2109
|
+
# """
|
|
2110
|
+
# Scan a local filesystem directory to preview what files would be indexed.
|
|
2111
|
+
#
|
|
2112
|
+
# This tool helps you understand what files will be processed before actually indexing.
|
|
2113
|
+
#
|
|
2114
|
+
# Args:
|
|
2115
|
+
# directory_path: Absolute path to the directory to scan
|
|
2116
|
+
# inclusion_patterns: Optional list of patterns to include (e.g., ["ext:.py", "dir:src"])
|
|
2117
|
+
# exclusion_patterns: Optional list of patterns to exclude (e.g., ["dir:node_modules", "ext:.log"])
|
|
2118
|
+
# max_file_size_mb: Maximum file size in MB to process (default: 50)
|
|
2119
|
+
#
|
|
2120
|
+
# Returns:
|
|
2121
|
+
# Summary of files that would be indexed including count, size, and file types
|
|
2122
|
+
# """
|
|
2123
|
+
# try:
|
|
2124
|
+
# # Validate absolute path
|
|
2125
|
+
# if not os.path.isabs(directory_path):
|
|
2126
|
+
# return [TextContent(
|
|
2127
|
+
# type="text",
|
|
2128
|
+
# text=f"❌ Error: directory_path must be an absolute path. Got: {directory_path}\n\n"
|
|
2129
|
+
# f"Example: /Users/username/projects/myproject"
|
|
2130
|
+
# )]
|
|
2131
|
+
#
|
|
2132
|
+
# client = await ensure_api_client()
|
|
2133
|
+
#
|
|
2134
|
+
# logger.info(f"Scanning local directory: {directory_path}")
|
|
2135
|
+
# result = await client.scan_local_filesystem(
|
|
2136
|
+
# directory_path=directory_path,
|
|
2137
|
+
# inclusion_patterns=inclusion_patterns or [],
|
|
2138
|
+
# exclusion_patterns=exclusion_patterns or [],
|
|
2139
|
+
# max_file_size_mb=max_file_size_mb
|
|
2140
|
+
# )
|
|
2141
|
+
#
|
|
2142
|
+
# # Format the scan results
|
|
2143
|
+
# total_files = result.get("total_files", 0)
|
|
2144
|
+
# total_size_mb = result.get("total_size_mb", 0)
|
|
2145
|
+
# file_types = result.get("file_types", {})
|
|
2146
|
+
# files = result.get("files", [])
|
|
2147
|
+
# truncated = result.get("truncated", False)
|
|
2148
|
+
#
|
|
2149
|
+
# response = f"📊 **Local Directory Scan Results**\n\n"
|
|
2150
|
+
# response += f"📁 **Directory:** `{directory_path}`\n"
|
|
2151
|
+
# response += f"📄 **Total Files:** {total_files:,}\n"
|
|
2152
|
+
# response += f"💾 **Total Size:** {total_size_mb:.2f} MB\n\n"
|
|
2153
|
+
#
|
|
2154
|
+
# if file_types:
|
|
2155
|
+
# response += "**File Types:**\n"
|
|
2156
|
+
# # Sort by count descending
|
|
2157
|
+
# sorted_types = sorted(file_types.items(), key=lambda x: x[1], reverse=True)
|
|
2158
|
+
# for ext, count in sorted_types[:10]: # Show top 10
|
|
2159
|
+
# response += f"• `{ext}`: {count:,} files\n"
|
|
2160
|
+
# if len(sorted_types) > 10:
|
|
2161
|
+
# response += f"• ... and {len(sorted_types) - 10} more types\n"
|
|
2162
|
+
# response += "\n"
|
|
2163
|
+
#
|
|
2164
|
+
# if files:
|
|
2165
|
+
# response += f"**Largest Files (showing {min(len(files), 10)}):**\n"
|
|
2166
|
+
# for i, file_info in enumerate(files[:10]):
|
|
2167
|
+
# size_mb = file_info["size"] / (1024 * 1024)
|
|
2168
|
+
# response += f"{i+1}. `{file_info['path']}` ({size_mb:.2f} MB)\n"
|
|
2169
|
+
#
|
|
2170
|
+
# if truncated:
|
|
2171
|
+
# response += f"\n*Note: Showing first 100 files out of {total_files:,} total*\n"
|
|
2172
|
+
#
|
|
2173
|
+
# if inclusion_patterns:
|
|
2174
|
+
# response += f"\n**Inclusion Patterns:** {', '.join(f'`{p}`' for p in inclusion_patterns)}\n"
|
|
2175
|
+
# if exclusion_patterns:
|
|
2176
|
+
# response += f"**Exclusion Patterns:** {', '.join(f'`{p}`' for p in exclusion_patterns)}\n"
|
|
2177
|
+
#
|
|
2178
|
+
# response += "\n💡 **Next Step:** Use `index_local_filesystem` to index these files."
|
|
2179
|
+
#
|
|
2180
|
+
# return [TextContent(type="text", text=response)]
|
|
2181
|
+
#
|
|
2182
|
+
# except APIError as e:
|
|
2183
|
+
# logger.error(f"API error scanning local filesystem: {e}")
|
|
2184
|
+
# return [TextContent(
|
|
2185
|
+
# type="text",
|
|
2186
|
+
# text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
2187
|
+
# )]
|
|
2188
|
+
# except Exception as e:
|
|
2189
|
+
# logger.error(f"Unexpected error scanning local filesystem: {e}")
|
|
2190
|
+
# return [TextContent(
|
|
2191
|
+
# type="text",
|
|
2192
|
+
# text=f"❌ Error: An unexpected error occurred while scanning: {str(e)}"
|
|
2193
|
+
# )]
|
|
2194
|
+
|
|
2195
|
+
# @mcp.tool()
|
|
2196
|
+
# async def check_local_filesystem_status(source_id: str) -> List[TextContent]:
|
|
2197
|
+
# """
|
|
2198
|
+
# Check the indexing status of a local filesystem source.
|
|
2199
|
+
#
|
|
2200
|
+
# Args:
|
|
2201
|
+
# source_id: The source ID returned when indexing was started
|
|
2202
|
+
#
|
|
2203
|
+
# Returns:
|
|
2204
|
+
# Current status of the local filesystem indexing
|
|
2205
|
+
# """
|
|
2206
|
+
# try:
|
|
2207
|
+
# client = await ensure_api_client()
|
|
2208
|
+
# status = await client.check_local_filesystem_status(source_id)
|
|
2209
|
+
#
|
|
2210
|
+
# # Format status response
|
|
2211
|
+
# status_text = status.get("status", "unknown")
|
|
2212
|
+
# progress = status.get("progress", 0)
|
|
2213
|
+
# message = status.get("message", "")
|
|
2214
|
+
# error = status.get("error")
|
|
2215
|
+
# directory_path = status.get("directory_path", "Unknown")
|
|
2216
|
+
# page_count = status.get("page_count", 0) # Number of files
|
|
2217
|
+
# chunk_count = status.get("chunk_count", 0)
|
|
2218
|
+
#
|
|
2219
|
+
# # Status emoji
|
|
2220
|
+
# status_emoji = {
|
|
2221
|
+
# "pending": "⏳",
|
|
2222
|
+
# "processing": "🔄",
|
|
2223
|
+
# "completed": "✅",
|
|
2224
|
+
# "failed": "❌",
|
|
2225
|
+
# "error": "❌"
|
|
2226
|
+
# }.get(status_text, "❓")
|
|
2227
|
+
#
|
|
2228
|
+
# response = f"{status_emoji} **Local Filesystem Status**\n\n"
|
|
2229
|
+
# response += f"🆔 **Source ID:** `{source_id}`\n"
|
|
2230
|
+
# response += f"📁 **Directory:** `{directory_path}`\n"
|
|
2231
|
+
# response += f"📊 **Status:** {status_text.capitalize()}\n"
|
|
2232
|
+
#
|
|
2233
|
+
# if progress > 0:
|
|
2234
|
+
# response += f"📈 **Progress:** {progress}%\n"
|
|
2235
|
+
#
|
|
2236
|
+
# if message:
|
|
2237
|
+
# response += f"💬 **Message:** {message}\n"
|
|
2238
|
+
#
|
|
2239
|
+
# if status_text == "completed":
|
|
2240
|
+
# response += f"\n✨ **Indexing Complete!**\n"
|
|
2241
|
+
# response += f"• **Files Indexed:** {page_count:,}\n"
|
|
2242
|
+
# response += f"• **Chunks Created:** {chunk_count:,}\n"
|
|
2243
|
+
# response += f"\nYou can now search this directory using `search_codebase` or the unified search!"
|
|
2244
|
+
# elif status_text in ["failed", "error"]:
|
|
2245
|
+
# response += f"\n❌ **Indexing Failed**\n"
|
|
2246
|
+
# if error:
|
|
2247
|
+
# response += f"**Error:** {error}\n"
|
|
2248
|
+
# response += "\nPlease check your directory path and try again."
|
|
2249
|
+
# elif status_text == "processing":
|
|
2250
|
+
# response += f"\n🔄 Indexing is in progress...\n"
|
|
2251
|
+
# response += "Check back in a few moments or monitor at [app.trynia.ai](https://app.trynia.ai)"
|
|
2252
|
+
#
|
|
2253
|
+
# return [TextContent(type="text", text=response)]
|
|
2254
|
+
#
|
|
2255
|
+
# except APIError as e:
|
|
2256
|
+
# logger.error(f"API error checking local filesystem status: {e}")
|
|
2257
|
+
# if e.status_code == 404:
|
|
2258
|
+
# return [TextContent(
|
|
2259
|
+
# type="text",
|
|
2260
|
+
# text=f"❌ Source ID `{source_id}` not found. Please check the ID and try again."
|
|
2261
|
+
# )]
|
|
2262
|
+
# return [TextContent(
|
|
2263
|
+
# type="text",
|
|
2264
|
+
# text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
2265
|
+
# )]
|
|
2266
|
+
# except Exception as e:
|
|
2267
|
+
# logger.error(f"Unexpected error checking local filesystem status: {e}")
|
|
2268
|
+
# return [TextContent(
|
|
2269
|
+
# type="text",
|
|
2270
|
+
# text=f"❌ Error: An unexpected error occurred: {str(e)}"
|
|
2271
|
+
# )]
|
|
2272
|
+
|
|
2273
|
+
# @mcp.tool()
|
|
2274
|
+
# async def search_local_filesystem(
|
|
2275
|
+
# source_id: str,
|
|
2276
|
+
# query: str,
|
|
2277
|
+
# include_sources: bool = True
|
|
2278
|
+
# ) -> List[TextContent]:
|
|
2279
|
+
# """
|
|
2280
|
+
# Search an indexed local filesystem directory using its source ID.
|
|
2281
|
+
#
|
|
2282
|
+
# To search local files:
|
|
2283
|
+
# 1. First index a directory using `index_local_filesystem` - this will return a source_id
|
|
2284
|
+
# 2. Use that source_id with this tool to search the indexed content
|
|
2285
|
+
#
|
|
2286
|
+
# Args:
|
|
2287
|
+
# source_id: The source ID returned when the directory was indexed (required)
|
|
2288
|
+
# query: Your search query in natural language (required)
|
|
2289
|
+
# include_sources: Whether to include source code snippets in results (default: True)
|
|
2290
|
+
#
|
|
2291
|
+
# Returns:
|
|
2292
|
+
# Search results with relevant file snippets and explanations
|
|
2293
|
+
#
|
|
2294
|
+
# Example:
|
|
2295
|
+
# # After indexing returns source_id "abc123-def456"
|
|
2296
|
+
# search_local_filesystem(
|
|
2297
|
+
# source_id="abc123-def456",
|
|
2298
|
+
# query="configuration settings"
|
|
2299
|
+
# )
|
|
2300
|
+
#
|
|
2301
|
+
# Note: To find your source IDs, use `list_documentation` and look for
|
|
2302
|
+
# sources with source_type="local_filesystem"
|
|
2303
|
+
# """
|
|
2304
|
+
# try:
|
|
2305
|
+
# # Validate inputs
|
|
2306
|
+
# if not source_id:
|
|
2307
|
+
# return [TextContent(
|
|
2308
|
+
# type="text",
|
|
2309
|
+
# text="❌ Error: 'source_id' parameter is required. Use the ID returned from index_local_filesystem."
|
|
2310
|
+
# )]
|
|
2311
|
+
#
|
|
2312
|
+
# if not query:
|
|
2313
|
+
# return [TextContent(
|
|
2314
|
+
# type="text",
|
|
2315
|
+
# text="❌ Error: 'query' parameter is required"
|
|
2316
|
+
# )]
|
|
2317
|
+
#
|
|
2318
|
+
# client = await ensure_api_client()
|
|
2319
|
+
#
|
|
2320
|
+
# # Check if the source exists and is ready
|
|
2321
|
+
# logger.info(f"Checking status of source {source_id}")
|
|
2322
|
+
# try:
|
|
2323
|
+
# status = await client.get_data_source_status(source_id)
|
|
2324
|
+
# if not status:
|
|
2325
|
+
# return [TextContent(
|
|
2326
|
+
# type="text",
|
|
2327
|
+
# text=f"❌ Source ID '{source_id}' not found. Please check the ID and try again."
|
|
2328
|
+
# )]
|
|
2329
|
+
#
|
|
2330
|
+
# source_status = status.get("status", "unknown")
|
|
2331
|
+
# if source_status == "processing":
|
|
2332
|
+
# progress = status.get("progress", 0)
|
|
2333
|
+
# return [TextContent(
|
|
2334
|
+
# type="text",
|
|
2335
|
+
# text=f"⏳ This source is still being indexed ({progress}% complete).\n\n"
|
|
2336
|
+
# f"Use `check_local_filesystem_status(\"{source_id}\")` to check progress."
|
|
2337
|
+
# )]
|
|
2338
|
+
# elif source_status == "failed":
|
|
2339
|
+
# error = status.get("error", "Unknown error")
|
|
2340
|
+
# return [TextContent(
|
|
2341
|
+
# type="text",
|
|
2342
|
+
# text=f"❌ This source failed to index.\n\nError: {error}"
|
|
2343
|
+
# )]
|
|
2344
|
+
# elif source_status != "completed":
|
|
2345
|
+
# return [TextContent(
|
|
2346
|
+
# type="text",
|
|
2347
|
+
# text=f"❌ Source is not ready for search. Status: {source_status}"
|
|
2348
|
+
# )]
|
|
2349
|
+
# except Exception as e:
|
|
2350
|
+
# logger.warning(f"Could not check source status: {e}")
|
|
2351
|
+
# # Continue anyway in case it's just a status check issue
|
|
2352
|
+
#
|
|
2353
|
+
# # Perform the search
|
|
2354
|
+
# logger.info(f"Searching local filesystem source {source_id} with query: {query}")
|
|
2355
|
+
#
|
|
2356
|
+
# # Use the unified query endpoint with data_sources parameter
|
|
2357
|
+
# result = client.query_unified(
|
|
2358
|
+
# messages=[{"role": "user", "content": query}],
|
|
2359
|
+
# data_sources=[source_id],
|
|
2360
|
+
# include_sources=include_sources,
|
|
2361
|
+
# stream=False
|
|
2362
|
+
# )
|
|
2363
|
+
#
|
|
2364
|
+
# # Parse the response
|
|
2365
|
+
# response_text = ""
|
|
2366
|
+
# async for chunk in result:
|
|
2367
|
+
# data = json.loads(chunk)
|
|
2368
|
+
# if "content" in data:
|
|
2369
|
+
# response_text = data["content"]
|
|
2370
|
+
# sources = data.get("sources", [])
|
|
2371
|
+
# break
|
|
2372
|
+
#
|
|
2373
|
+
# # Format the response nicely for local filesystem results
|
|
2374
|
+
# if response_text:
|
|
2375
|
+
# # Extract the local filesystem results section if present
|
|
2376
|
+
# if "**Local filesystem results" in response_text:
|
|
2377
|
+
# # Keep the original response
|
|
2378
|
+
# formatted_response = response_text
|
|
2379
|
+
# else:
|
|
2380
|
+
# # Create our own formatted response
|
|
2381
|
+
# formatted_response = f"🔍 **Search Results for Local Directory**\n"
|
|
2382
|
+
# formatted_response += f"🔎 Query: \"{query}\"\n\n"
|
|
2383
|
+
# formatted_response += response_text
|
|
2384
|
+
#
|
|
2385
|
+
# # Add sources if available and requested
|
|
2386
|
+
# if include_sources and sources:
|
|
2387
|
+
# formatted_response += "\n\n**📄 Source Details:**\n"
|
|
2388
|
+
# for i, source in enumerate(sources[:5], 1):
|
|
2389
|
+
# metadata = source.get("metadata", {})
|
|
2390
|
+
# file_path = metadata.get("file_path", "Unknown file")
|
|
2391
|
+
# formatted_response += f"\n{i}. `{file_path}`\n"
|
|
2392
|
+
#
|
|
2393
|
+
# # Add snippet of content
|
|
2394
|
+
# content = source.get("content", "")
|
|
2395
|
+
# if content:
|
|
2396
|
+
# # Truncate to reasonable length
|
|
2397
|
+
# lines = content.split('\n')[:10]
|
|
2398
|
+
# snippet = '\n'.join(lines)
|
|
2399
|
+
# if len(lines) > 10:
|
|
2400
|
+
# snippet += "\n..."
|
|
2401
|
+
# formatted_response += f"```\n{snippet}\n```\n"
|
|
2402
|
+
#
|
|
2403
|
+
# return [TextContent(type="text", text=formatted_response)]
|
|
2404
|
+
# else:
|
|
2405
|
+
# return [TextContent(
|
|
2406
|
+
# type="text",
|
|
2407
|
+
# text=f"No results found for query: \"{query}\" in the indexed directory."
|
|
2408
|
+
# )]
|
|
2409
|
+
#
|
|
2410
|
+
# except APIError as e:
|
|
2411
|
+
# logger.error(f"API error searching local filesystem: {e}")
|
|
2412
|
+
# return [TextContent(
|
|
2413
|
+
# type="text",
|
|
2414
|
+
# text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
2415
|
+
# )]
|
|
2416
|
+
# except Exception as e:
|
|
2417
|
+
# logger.error(f"Unexpected error searching local filesystem: {e}")
|
|
2418
|
+
# return [TextContent(
|
|
2419
|
+
# type="text",
|
|
2420
|
+
# text=f"❌ Error: An unexpected error occurred: {str(e)}"
|
|
2421
|
+
# )]
|
|
2422
|
+
|
|
2423
|
+
# ===============================================================================
|
|
2424
|
+
# CHROMA PACKAGE SEARCH INTEGRATION
|
|
2425
|
+
# ===============================================================================
|
|
2426
|
+
#
|
|
2427
|
+
# Provides access to Chroma's Package Search MCP tools for searching actual
|
|
2428
|
+
# source code from 3,000+ packages across multiple package registries.
|
|
2429
|
+
# This integration enables AI assistants to search ground-truth code instead
|
|
2430
|
+
# of relying on training data or hallucinations.
|
|
2431
|
+
#
|
|
2432
|
+
# Available Registries:
|
|
2433
|
+
# - py_pi: Python Package Index (PyPI) packages
|
|
2434
|
+
# - npm: Node.js packages from NPM registry
|
|
2435
|
+
# - crates_io: Rust packages from crates.io
|
|
2436
|
+
# - golang_proxy: Go modules from Go proxy
|
|
2437
|
+
#
|
|
2438
|
+
# Authentication:
|
|
2439
|
+
# - Requires CHROMA_API_KEY environment variable
|
|
2440
|
+
# - Uses x-chroma-token header for API authentication
|
|
2441
|
+
#
|
|
2442
|
+
# Tools:
|
|
2443
|
+
# 1. nia_package_search_grep: Regex-based code search
|
|
2444
|
+
# 2. nia_package_search_hybrid: Semantic/AI-powered search
|
|
2445
|
+
# 3. nia_package_search_read_file: Direct file content retrieval
|
|
2446
|
+
#
|
|
2447
|
+
# ===============================================================================
|
|
2448
|
+
|
|
1636
2449
|
@mcp.tool()
|
|
1637
|
-
async def
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2450
|
+
async def nia_package_search_grep(
|
|
2451
|
+
registry: str,
|
|
2452
|
+
package_name: str,
|
|
2453
|
+
pattern: str,
|
|
2454
|
+
version: Optional[str] = None,
|
|
2455
|
+
language: Optional[str] = None,
|
|
2456
|
+
filename_sha256: Optional[str] = None,
|
|
2457
|
+
a: Optional[int] = None,
|
|
2458
|
+
b: Optional[int] = None,
|
|
2459
|
+
c: Optional[int] = None,
|
|
2460
|
+
head_limit: Optional[int] = None,
|
|
2461
|
+
output_mode: str = "content"
|
|
1642
2462
|
) -> List[TextContent]:
|
|
1643
2463
|
"""
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2464
|
+
Executes a grep over the source code of a public package. This tool is useful for deterministically
|
|
2465
|
+
finding code in a package using regex. Use this tool before implementing solutions that use external
|
|
2466
|
+
packages. The regex pattern should be restrictive enough to only match code you're looking for, to limit
|
|
2467
|
+
overfetching.
|
|
2468
|
+
|
|
2469
|
+
Required Args: "registry", "package_name", "pattern" Optional Args: "version", "language",
|
|
2470
|
+
"filename_sha256", "a", "b", "c", "head_limit", "output_mode"
|
|
2471
|
+
|
|
2472
|
+
Best for: Deterministic code search, finding specific code patterns, or exploring code structure.
|
|
2473
|
+
|
|
2474
|
+
Parameters:
|
|
2475
|
+
a: The number of lines after a grep match to include
|
|
2476
|
+
b: The number of lines before a grep match to include
|
|
2477
|
+
c: The number of lines before and after a grep match to include
|
|
2478
|
+
filename_sha256: The sha256 hash of the file to filter for
|
|
2479
|
+
head_limit: Limits number of results returned. If the number of results returned is less than the
|
|
2480
|
+
head limit, all results have been returned.
|
|
2481
|
+
language: The languages to filter for. If not provided, all languages will be searched. Valid
|
|
2482
|
+
options: "Rust", "Go", "Python", "JavaScript", "JSX", "TypeScript", "TSX", "HTML", "Markdown",
|
|
2483
|
+
"YAML", "Bash", "SQL", "JSON", "Text", "Dockerfile", "HCL", "Protobuf", "Make", "Toml", "Jupyter Notebook"
|
|
2484
|
+
output_mode: Controls the shape of the grep output. Accepted values:
|
|
2485
|
+
"content" (default): return content snippets with line ranges
|
|
2486
|
+
"files_with_matches": return unique files (path and sha256) that match
|
|
2487
|
+
"count": return files with the count of matches per file
|
|
2488
|
+
package_name: The name of the requested package. Pass the name as it appears in the package
|
|
2489
|
+
manager. For Go packages, use the GitHub organization and repository name in the format
|
|
2490
|
+
{org}/{repo}, if unsure check the GitHub URL for the package and use {org}/{repo} from that URL.
|
|
2491
|
+
pattern: The regex pattern for exact text matching in the codebase. Must be a valid regex.
|
|
2492
|
+
Example: "func\\s+\\(get_repository\\|getRepository\\)\\s*\\(.*?\\)\\s\\{"
|
|
2493
|
+
registry: The name of the registry containing the requested package. Must be one of:
|
|
2494
|
+
"crates_io", "golang_proxy", "npm", or "py_pi".
|
|
2495
|
+
version: Optionally, the specific version of the package whose source code to search.
|
|
2496
|
+
If provided, must be in semver format: {major}.{minor}.{patch}. Otherwise, the latest indexed
|
|
2497
|
+
version of the package available will be used.
|
|
1658
2498
|
"""
|
|
1659
2499
|
try:
|
|
1660
|
-
#
|
|
1661
|
-
if not os.path.isabs(directory_path):
|
|
1662
|
-
return [TextContent(
|
|
1663
|
-
type="text",
|
|
1664
|
-
text=f"❌ Error: directory_path must be an absolute path. Got: {directory_path}\n\n"
|
|
1665
|
-
f"Example: /Users/username/projects/myproject"
|
|
1666
|
-
)]
|
|
1667
|
-
|
|
2500
|
+
# Use API client for backend routing
|
|
1668
2501
|
client = await ensure_api_client()
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
result = await client.
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2502
|
+
logger.info(f"Searching package {package_name} from {registry} with pattern: {pattern}")
|
|
2503
|
+
|
|
2504
|
+
# Execute grep search through backend
|
|
2505
|
+
result = await client.package_search_grep(
|
|
2506
|
+
registry=registry,
|
|
2507
|
+
package_name=package_name,
|
|
2508
|
+
pattern=pattern,
|
|
2509
|
+
version=version,
|
|
2510
|
+
language=language,
|
|
2511
|
+
filename_sha256=filename_sha256,
|
|
2512
|
+
a=a,
|
|
2513
|
+
b=b,
|
|
2514
|
+
c=c,
|
|
2515
|
+
head_limit=head_limit,
|
|
2516
|
+
output_mode=output_mode
|
|
1677
2517
|
)
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
status_url = result["data"]["status_url"]
|
|
1682
|
-
|
|
2518
|
+
|
|
2519
|
+
# Handle raw Chroma JSON response
|
|
2520
|
+
if not result or not isinstance(result, dict):
|
|
1683
2521
|
return [TextContent(
|
|
1684
2522
|
type="text",
|
|
1685
|
-
text=(
|
|
1686
|
-
f"✅ Successfully started indexing local directory!\n\n"
|
|
1687
|
-
f"📁 **Directory:** `{directory_path}`\n"
|
|
1688
|
-
f"🆔 **Source ID:** `{source_id}`\n"
|
|
1689
|
-
f"📊 **Status:** Processing\n\n"
|
|
1690
|
-
f"**What happens next:**\n"
|
|
1691
|
-
f"• NIA is scanning and indexing your files in the background\n"
|
|
1692
|
-
f"• This process typically takes a few minutes depending on directory size\n"
|
|
1693
|
-
f"• Use `check_local_filesystem_status` with source ID `{source_id}` to monitor progress\n"
|
|
1694
|
-
f"• Once indexed, use `search_codebase` or `search_documentation` to search your files\n\n"
|
|
1695
|
-
f"📌 **Tip:** You can check the status at any time or visit [app.trynia.ai](https://app.trynia.ai) to monitor progress."
|
|
1696
|
-
)
|
|
2523
|
+
text=f"No response from Chroma for pattern '{pattern}' in {package_name} ({registry})"
|
|
1697
2524
|
)]
|
|
1698
|
-
|
|
2525
|
+
|
|
2526
|
+
# Extract results and version from raw Chroma response
|
|
2527
|
+
results = result.get("results", [])
|
|
2528
|
+
version_used = result.get("version_used")
|
|
2529
|
+
|
|
2530
|
+
if not results:
|
|
1699
2531
|
return [TextContent(
|
|
1700
2532
|
type="text",
|
|
1701
|
-
text=f"
|
|
2533
|
+
text=f"No matches found for pattern '{pattern}' in {package_name} ({registry})"
|
|
1702
2534
|
)]
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
2535
|
+
|
|
2536
|
+
response_lines = [
|
|
2537
|
+
f"# 🔍 Package Search Results: {package_name} ({registry})",
|
|
2538
|
+
f"**Pattern:** `{pattern}`",
|
|
2539
|
+
""
|
|
2540
|
+
]
|
|
2541
|
+
|
|
2542
|
+
if version_used:
|
|
2543
|
+
response_lines.append(f"**Version:** {version_used}")
|
|
2544
|
+
elif version:
|
|
2545
|
+
response_lines.append(f"**Version:** {version}")
|
|
2546
|
+
|
|
2547
|
+
response_lines.append(f"**Found {len(results)} matches**\n")
|
|
2548
|
+
|
|
2549
|
+
# Handle grep result format: {output_mode: "content", result: {content, file_path, start_line, etc}}
|
|
2550
|
+
for i, item in enumerate(results, 1):
|
|
2551
|
+
response_lines.append(f"## Match {i}")
|
|
2552
|
+
|
|
2553
|
+
# Extract data from Chroma grep format
|
|
2554
|
+
if "result" in item:
|
|
2555
|
+
result_data = item["result"]
|
|
2556
|
+
if result_data.get("file_path"):
|
|
2557
|
+
response_lines.append(f"**File:** `{result_data['file_path']}`")
|
|
2558
|
+
|
|
2559
|
+
# Show SHA256 for read_file tool usage
|
|
2560
|
+
if result_data.get("filename_sha256"):
|
|
2561
|
+
response_lines.append(f"**SHA256:** `{result_data['filename_sha256']}`")
|
|
2562
|
+
|
|
2563
|
+
if result_data.get("start_line") and result_data.get("end_line"):
|
|
2564
|
+
response_lines.append(f"**Lines:** {result_data['start_line']}-{result_data['end_line']}")
|
|
2565
|
+
if result_data.get("language"):
|
|
2566
|
+
response_lines.append(f"**Language:** {result_data['language']}")
|
|
2567
|
+
|
|
2568
|
+
response_lines.append("```")
|
|
2569
|
+
response_lines.append(result_data.get("content", ""))
|
|
2570
|
+
response_lines.append("```\n")
|
|
2571
|
+
else:
|
|
2572
|
+
# Fallback for other formats
|
|
2573
|
+
response_lines.append("```")
|
|
2574
|
+
response_lines.append(str(item))
|
|
2575
|
+
response_lines.append("```\n")
|
|
2576
|
+
|
|
2577
|
+
# Add truncation message if present
|
|
2578
|
+
if result.get("truncation_message"):
|
|
2579
|
+
response_lines.append(f"⚠️ **Note:** {result['truncation_message']}")
|
|
2580
|
+
|
|
2581
|
+
# Add usage hint for read_file workflow (grep tool)
|
|
2582
|
+
response_lines.append("\n💡 **To read full file content:**")
|
|
2583
|
+
response_lines.append("Copy a SHA256 above and use: `nia_package_search_read_file(registry=..., package_name=..., filename_sha256=\"...\", start_line=1, end_line=100)`")
|
|
2584
|
+
|
|
2585
|
+
return [TextContent(type="text", text="\n".join(response_lines))]
|
|
2586
|
+
|
|
1710
2587
|
except Exception as e:
|
|
1711
|
-
logger.error(f"
|
|
2588
|
+
logger.error(f"Error in package search grep: {e}")
|
|
1712
2589
|
return [TextContent(
|
|
1713
2590
|
type="text",
|
|
1714
|
-
text=f"❌ Error
|
|
2591
|
+
text=f"❌ Error searching package: {str(e)}\n\n"
|
|
2592
|
+
f"Make sure:\n"
|
|
2593
|
+
f"- The registry is one of: crates_io, golang_proxy, npm, py_pi\n"
|
|
2594
|
+
f"- The package name is correct\n"
|
|
2595
|
+
f"- The pattern is a valid regex"
|
|
1715
2596
|
)]
|
|
1716
2597
|
|
|
1717
2598
|
@mcp.tool()
|
|
1718
|
-
async def
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2599
|
+
async def nia_package_search_hybrid(
|
|
2600
|
+
registry: str,
|
|
2601
|
+
package_name: str,
|
|
2602
|
+
semantic_queries: List[str],
|
|
2603
|
+
version: Optional[str] = None,
|
|
2604
|
+
filename_sha256: Optional[str] = None,
|
|
2605
|
+
pattern: Optional[str] = None,
|
|
2606
|
+
language: Optional[str] = None
|
|
1723
2607
|
) -> List[TextContent]:
|
|
1724
2608
|
"""
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2609
|
+
Searches package source code using semantic understanding AND optionally regex patterns. This
|
|
2610
|
+
allows for hybrid search, allowing for prefiltering with regex, and semantic ranking.
|
|
2611
|
+
|
|
2612
|
+
Required Args: "registry", "package_name", "semantic_queries"
|
|
2613
|
+
|
|
2614
|
+
Optional Args: "version", "filename_sha256", "pattern", "language"
|
|
2615
|
+
|
|
2616
|
+
Best for: Understanding how packages implement specific features, finding usage patterns, or
|
|
2617
|
+
exploring code structure.
|
|
2618
|
+
|
|
2619
|
+
Parameters:
|
|
2620
|
+
filename_sha256: The sha256 hash of the file to filter for
|
|
2621
|
+
language: The languages to filter for. If not provided, all languages will be searched. Valid
|
|
2622
|
+
options: "Rust", "Go", "Python", "JavaScript", "JSX", "TypeScript", "TSX", "HTML", "Markdown",
|
|
2623
|
+
"YAML", "Bash", "SQL", "JSON", "Text", "Dockerfile", "HCL", "Protobuf", "Make", "Toml", "Jupyter Notebook"
|
|
2624
|
+
package_name: The name of the requested package. Pass the name as it appears in the package
|
|
2625
|
+
manager. For Go packages, use the GitHub organization and repository name in the format
|
|
2626
|
+
{org}/{repo}, if unsure check the GitHub URL for the package and use {org}/{repo} from that URL.
|
|
2627
|
+
pattern: The regex pattern for exact text matching in the codebase. Must be a valid regex.
|
|
2628
|
+
Example: "func\\s+\\(get_repository\\|getRepository\\)\\s*\\(.*?\\)\\s\\{"
|
|
2629
|
+
registry: The name of the registry containing the requested package. Must be one of:
|
|
2630
|
+
"crates_io", "golang_proxy", "npm", or "py_pi".
|
|
2631
|
+
semantic_queries: Array of 1-5 plain English questions about the codebase. Example: ["how is
|
|
2632
|
+
argmax implemented in numpy?", "what testing patterns does axum use?"]
|
|
2633
|
+
version: Optionally, the specific version of the package whose source code to search.
|
|
2634
|
+
If provided, must be in semver format: {major}.{minor}.{patch}. Otherwise, the latest indexed
|
|
2635
|
+
version of the package available will be used.
|
|
1737
2636
|
"""
|
|
1738
2637
|
try:
|
|
1739
|
-
#
|
|
1740
|
-
|
|
2638
|
+
# Use API client for backend routing
|
|
2639
|
+
client = await ensure_api_client()
|
|
2640
|
+
logger.info(f"Hybrid search in {package_name} from {registry} with queries: {semantic_queries}")
|
|
2641
|
+
|
|
2642
|
+
# Execute hybrid search through backend
|
|
2643
|
+
result = await client.package_search_hybrid(
|
|
2644
|
+
registry=registry,
|
|
2645
|
+
package_name=package_name,
|
|
2646
|
+
semantic_queries=semantic_queries,
|
|
2647
|
+
version=version,
|
|
2648
|
+
filename_sha256=filename_sha256,
|
|
2649
|
+
pattern=pattern,
|
|
2650
|
+
language=language
|
|
2651
|
+
)
|
|
2652
|
+
|
|
2653
|
+
# Handle raw Chroma JSON response
|
|
2654
|
+
if not result or not isinstance(result, dict):
|
|
2655
|
+
queries_str = "\n".join(f"- {q}" for q in semantic_queries)
|
|
1741
2656
|
return [TextContent(
|
|
1742
2657
|
type="text",
|
|
1743
|
-
text=f"
|
|
1744
|
-
f"Example: /Users/username/projects/myproject"
|
|
2658
|
+
text=f"No response from Chroma for queries:\n{queries_str}\n\nin {package_name} ({registry})"
|
|
1745
2659
|
)]
|
|
1746
|
-
|
|
1747
|
-
client = await ensure_api_client()
|
|
1748
|
-
|
|
1749
|
-
logger.info(f"Scanning local directory: {directory_path}")
|
|
1750
|
-
result = await client.scan_local_filesystem(
|
|
1751
|
-
directory_path=directory_path,
|
|
1752
|
-
inclusion_patterns=inclusion_patterns or [],
|
|
1753
|
-
exclusion_patterns=exclusion_patterns or [],
|
|
1754
|
-
max_file_size_mb=max_file_size_mb
|
|
1755
|
-
)
|
|
1756
|
-
|
|
1757
|
-
# Format the scan results
|
|
1758
|
-
total_files = result.get("total_files", 0)
|
|
1759
|
-
total_size_mb = result.get("total_size_mb", 0)
|
|
1760
|
-
file_types = result.get("file_types", {})
|
|
1761
|
-
files = result.get("files", [])
|
|
1762
|
-
truncated = result.get("truncated", False)
|
|
1763
|
-
|
|
1764
|
-
response = f"📊 **Local Directory Scan Results**\n\n"
|
|
1765
|
-
response += f"📁 **Directory:** `{directory_path}`\n"
|
|
1766
|
-
response += f"📄 **Total Files:** {total_files:,}\n"
|
|
1767
|
-
response += f"💾 **Total Size:** {total_size_mb:.2f} MB\n\n"
|
|
1768
|
-
|
|
1769
|
-
if file_types:
|
|
1770
|
-
response += "**File Types:**\n"
|
|
1771
|
-
# Sort by count descending
|
|
1772
|
-
sorted_types = sorted(file_types.items(), key=lambda x: x[1], reverse=True)
|
|
1773
|
-
for ext, count in sorted_types[:10]: # Show top 10
|
|
1774
|
-
response += f"• `{ext}`: {count:,} files\n"
|
|
1775
|
-
if len(sorted_types) > 10:
|
|
1776
|
-
response += f"• ... and {len(sorted_types) - 10} more types\n"
|
|
1777
|
-
response += "\n"
|
|
1778
|
-
|
|
1779
|
-
if files:
|
|
1780
|
-
response += f"**Largest Files (showing {min(len(files), 10)}):**\n"
|
|
1781
|
-
for i, file_info in enumerate(files[:10]):
|
|
1782
|
-
size_mb = file_info["size"] / (1024 * 1024)
|
|
1783
|
-
response += f"{i+1}. `{file_info['path']}` ({size_mb:.2f} MB)\n"
|
|
1784
|
-
|
|
1785
|
-
if truncated:
|
|
1786
|
-
response += f"\n*Note: Showing first 100 files out of {total_files:,} total*\n"
|
|
1787
|
-
|
|
1788
|
-
if inclusion_patterns:
|
|
1789
|
-
response += f"\n**Inclusion Patterns:** {', '.join(f'`{p}`' for p in inclusion_patterns)}\n"
|
|
1790
|
-
if exclusion_patterns:
|
|
1791
|
-
response += f"**Exclusion Patterns:** {', '.join(f'`{p}`' for p in exclusion_patterns)}\n"
|
|
1792
|
-
|
|
1793
|
-
response += "\n💡 **Next Step:** Use `index_local_filesystem` to index these files."
|
|
1794
|
-
|
|
1795
|
-
return [TextContent(type="text", text=response)]
|
|
1796
|
-
|
|
1797
|
-
except APIError as e:
|
|
1798
|
-
logger.error(f"API error scanning local filesystem: {e}")
|
|
1799
|
-
return [TextContent(
|
|
1800
|
-
type="text",
|
|
1801
|
-
text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
1802
|
-
)]
|
|
1803
|
-
except Exception as e:
|
|
1804
|
-
logger.error(f"Unexpected error scanning local filesystem: {e}")
|
|
1805
|
-
return [TextContent(
|
|
1806
|
-
type="text",
|
|
1807
|
-
text=f"❌ Error: An unexpected error occurred while scanning: {str(e)}"
|
|
1808
|
-
)]
|
|
1809
2660
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
source_id: The source ID returned when indexing was started
|
|
1817
|
-
|
|
1818
|
-
Returns:
|
|
1819
|
-
Current status of the local filesystem indexing
|
|
1820
|
-
"""
|
|
1821
|
-
try:
|
|
1822
|
-
client = await ensure_api_client()
|
|
1823
|
-
status = await client.check_local_filesystem_status(source_id)
|
|
1824
|
-
|
|
1825
|
-
# Format status response
|
|
1826
|
-
status_text = status.get("status", "unknown")
|
|
1827
|
-
progress = status.get("progress", 0)
|
|
1828
|
-
message = status.get("message", "")
|
|
1829
|
-
error = status.get("error")
|
|
1830
|
-
directory_path = status.get("directory_path", "Unknown")
|
|
1831
|
-
page_count = status.get("page_count", 0) # Number of files
|
|
1832
|
-
chunk_count = status.get("chunk_count", 0)
|
|
1833
|
-
|
|
1834
|
-
# Status emoji
|
|
1835
|
-
status_emoji = {
|
|
1836
|
-
"pending": "⏳",
|
|
1837
|
-
"processing": "🔄",
|
|
1838
|
-
"completed": "✅",
|
|
1839
|
-
"failed": "❌",
|
|
1840
|
-
"error": "❌"
|
|
1841
|
-
}.get(status_text, "❓")
|
|
1842
|
-
|
|
1843
|
-
response = f"{status_emoji} **Local Filesystem Status**\n\n"
|
|
1844
|
-
response += f"🆔 **Source ID:** `{source_id}`\n"
|
|
1845
|
-
response += f"📁 **Directory:** `{directory_path}`\n"
|
|
1846
|
-
response += f"📊 **Status:** {status_text.capitalize()}\n"
|
|
1847
|
-
|
|
1848
|
-
if progress > 0:
|
|
1849
|
-
response += f"📈 **Progress:** {progress}%\n"
|
|
1850
|
-
|
|
1851
|
-
if message:
|
|
1852
|
-
response += f"💬 **Message:** {message}\n"
|
|
1853
|
-
|
|
1854
|
-
if status_text == "completed":
|
|
1855
|
-
response += f"\n✨ **Indexing Complete!**\n"
|
|
1856
|
-
response += f"• **Files Indexed:** {page_count:,}\n"
|
|
1857
|
-
response += f"• **Chunks Created:** {chunk_count:,}\n"
|
|
1858
|
-
response += f"\nYou can now search this directory using `search_codebase` or the unified search!"
|
|
1859
|
-
elif status_text in ["failed", "error"]:
|
|
1860
|
-
response += f"\n❌ **Indexing Failed**\n"
|
|
1861
|
-
if error:
|
|
1862
|
-
response += f"**Error:** {error}\n"
|
|
1863
|
-
response += "\nPlease check your directory path and try again."
|
|
1864
|
-
elif status_text == "processing":
|
|
1865
|
-
response += f"\n🔄 Indexing is in progress...\n"
|
|
1866
|
-
response += "Check back in a few moments or monitor at [app.trynia.ai](https://app.trynia.ai)"
|
|
1867
|
-
|
|
1868
|
-
return [TextContent(type="text", text=response)]
|
|
1869
|
-
|
|
1870
|
-
except APIError as e:
|
|
1871
|
-
logger.error(f"API error checking local filesystem status: {e}")
|
|
1872
|
-
if e.status_code == 404:
|
|
2661
|
+
# Extract results and version from raw Chroma response
|
|
2662
|
+
results = result.get("results", [])
|
|
2663
|
+
version_used = result.get("version_used")
|
|
2664
|
+
|
|
2665
|
+
if not results:
|
|
2666
|
+
queries_str = "\n".join(f"- {q}" for q in semantic_queries)
|
|
1873
2667
|
return [TextContent(
|
|
1874
2668
|
type="text",
|
|
1875
|
-
text=f"
|
|
2669
|
+
text=f"No relevant code found for queries:\n{queries_str}\n\nin {package_name} ({registry})"
|
|
1876
2670
|
)]
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2671
|
+
|
|
2672
|
+
response_lines = [
|
|
2673
|
+
f"# 🔎 Package Semantic Search: {package_name} ({registry})",
|
|
2674
|
+
"**Queries:**"
|
|
2675
|
+
]
|
|
2676
|
+
|
|
2677
|
+
for query in semantic_queries:
|
|
2678
|
+
response_lines.append(f"- {query}")
|
|
2679
|
+
|
|
2680
|
+
response_lines.append("")
|
|
2681
|
+
|
|
2682
|
+
if version_used:
|
|
2683
|
+
response_lines.append(f"**Version:** {version_used}")
|
|
2684
|
+
elif version:
|
|
2685
|
+
response_lines.append(f"**Version:** {version}")
|
|
2686
|
+
if pattern:
|
|
2687
|
+
response_lines.append(f"**Pattern Filter:** `{pattern}`")
|
|
2688
|
+
|
|
2689
|
+
response_lines.append(f"\n**Found {len(results)} relevant code sections**\n")
|
|
2690
|
+
|
|
2691
|
+
# Handle hybrid result format: {id: "...", document: "content", metadata: {...}}
|
|
2692
|
+
for i, item in enumerate(results, 1):
|
|
2693
|
+
response_lines.append(f"## Result {i}")
|
|
2694
|
+
|
|
2695
|
+
# Extract metadata if available
|
|
2696
|
+
metadata = item.get("metadata", {})
|
|
2697
|
+
if metadata.get("filename"):
|
|
2698
|
+
response_lines.append(f"**File:** `{metadata['filename']}`")
|
|
2699
|
+
|
|
2700
|
+
# Show SHA256 for read_file tool usage (from metadata)
|
|
2701
|
+
if metadata.get("filename_sha256"):
|
|
2702
|
+
response_lines.append(f"**SHA256:** `{metadata['filename_sha256']}`")
|
|
2703
|
+
|
|
2704
|
+
if metadata.get("start_line") and metadata.get("end_line"):
|
|
2705
|
+
response_lines.append(f"**Lines:** {metadata['start_line']}-{metadata['end_line']}")
|
|
2706
|
+
if metadata.get("language"):
|
|
2707
|
+
response_lines.append(f"**Language:** {metadata['language']}")
|
|
2708
|
+
|
|
2709
|
+
# Get document content
|
|
2710
|
+
content = item.get("document", "")
|
|
2711
|
+
if content:
|
|
2712
|
+
response_lines.append("```")
|
|
2713
|
+
response_lines.append(content)
|
|
2714
|
+
response_lines.append("```\n")
|
|
2715
|
+
|
|
2716
|
+
# Add truncation message if present
|
|
2717
|
+
if result.get("truncation_message"):
|
|
2718
|
+
response_lines.append(f"⚠️ **Note:** {result['truncation_message']}")
|
|
2719
|
+
|
|
2720
|
+
# Add usage hint for read_file workflow (hybrid tool)
|
|
2721
|
+
response_lines.append("\n💡 **To read full file content:**")
|
|
2722
|
+
response_lines.append("Copy a SHA256 above and use: `nia_package_search_read_file(registry=..., package_name=..., filename_sha256=\"...\", start_line=1, end_line=100)`")
|
|
2723
|
+
|
|
2724
|
+
return [TextContent(type="text", text="\n".join(response_lines))]
|
|
2725
|
+
|
|
1881
2726
|
except Exception as e:
|
|
1882
|
-
logger.error(f"
|
|
2727
|
+
logger.error(f"Error in package search hybrid: {e}")
|
|
1883
2728
|
return [TextContent(
|
|
1884
2729
|
type="text",
|
|
1885
|
-
text=f"❌ Error
|
|
2730
|
+
text=f"❌ Error in hybrid search: {str(e)}\n\n"
|
|
2731
|
+
f"Make sure:\n"
|
|
2732
|
+
f"- The registry is one of: crates_io, golang_proxy, npm, py_pi\n"
|
|
2733
|
+
f"- The package name is correct\n"
|
|
2734
|
+
f"- Semantic queries are provided (1-5 queries)"
|
|
1886
2735
|
)]
|
|
1887
2736
|
|
|
1888
2737
|
@mcp.tool()
|
|
1889
|
-
async def
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2738
|
+
async def nia_package_search_read_file(
|
|
2739
|
+
registry: str,
|
|
2740
|
+
package_name: str,
|
|
2741
|
+
filename_sha256: str,
|
|
2742
|
+
start_line: int,
|
|
2743
|
+
end_line: int,
|
|
2744
|
+
version: Optional[str] = None
|
|
1893
2745
|
) -> List[TextContent]:
|
|
1894
2746
|
"""
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
Note: To find your source IDs, use `list_documentation` and look for
|
|
1917
|
-
sources with source_type="local_filesystem"
|
|
2747
|
+
Reads exact lines from a source file of a public package. Useful for fetching specific code regions by
|
|
2748
|
+
line range.
|
|
2749
|
+
|
|
2750
|
+
Required Args: "registry", "package_name", "filename_sha256", "start_line", "end_line" Optional Args:
|
|
2751
|
+
"version"
|
|
2752
|
+
|
|
2753
|
+
Best for: Inspecting exact code snippets when you already know the file and line numbers. Max 200
|
|
2754
|
+
lines.
|
|
2755
|
+
|
|
2756
|
+
Parameters:
|
|
2757
|
+
end_line: 1-based inclusive end line to read
|
|
2758
|
+
filename_sha256: The sha256 hash of the file to filter for
|
|
2759
|
+
package_name: The name of the requested package. Pass the name as it appears in the package
|
|
2760
|
+
manager. For Go packages, use the GitHub organization and repository name in the format
|
|
2761
|
+
{org}/{repo}, if unsure check the GitHub URL for the package and use {org}/{repo} from that URL.
|
|
2762
|
+
registry: The name of the registry containing the requested package. Must be one of:
|
|
2763
|
+
"crates_io", "golang_proxy", "npm", or "py_pi".
|
|
2764
|
+
start_line: 1-based inclusive start line to read
|
|
2765
|
+
version: Optionally, the specific version of the package whose source code to search.
|
|
2766
|
+
If provided, must be in semver format: {major}.{minor}.{patch}. Otherwise, the latest indexed
|
|
2767
|
+
version of the package available will be used.
|
|
1918
2768
|
"""
|
|
1919
2769
|
try:
|
|
1920
|
-
# Validate
|
|
1921
|
-
if
|
|
2770
|
+
# Validate line range
|
|
2771
|
+
if end_line - start_line + 1 > 200:
|
|
1922
2772
|
return [TextContent(
|
|
1923
2773
|
type="text",
|
|
1924
|
-
text="❌ Error:
|
|
2774
|
+
text="❌ Error: Maximum 200 lines can be read at once. Please reduce the line range."
|
|
1925
2775
|
)]
|
|
1926
|
-
|
|
1927
|
-
if
|
|
2776
|
+
|
|
2777
|
+
if start_line < 1 or end_line < start_line:
|
|
1928
2778
|
return [TextContent(
|
|
1929
2779
|
type="text",
|
|
1930
|
-
text="❌ Error:
|
|
2780
|
+
text="❌ Error: Invalid line range. Start line must be >= 1 and end line must be >= start line."
|
|
1931
2781
|
)]
|
|
1932
|
-
|
|
2782
|
+
|
|
2783
|
+
# Use API client for backend routing
|
|
1933
2784
|
client = await ensure_api_client()
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
source_status = status.get("status", "unknown")
|
|
1946
|
-
if source_status == "processing":
|
|
1947
|
-
progress = status.get("progress", 0)
|
|
1948
|
-
return [TextContent(
|
|
1949
|
-
type="text",
|
|
1950
|
-
text=f"⏳ This source is still being indexed ({progress}% complete).\n\n"
|
|
1951
|
-
f"Use `check_local_filesystem_status(\"{source_id}\")` to check progress."
|
|
1952
|
-
)]
|
|
1953
|
-
elif source_status == "failed":
|
|
1954
|
-
error = status.get("error", "Unknown error")
|
|
1955
|
-
return [TextContent(
|
|
1956
|
-
type="text",
|
|
1957
|
-
text=f"❌ This source failed to index.\n\nError: {error}"
|
|
1958
|
-
)]
|
|
1959
|
-
elif source_status != "completed":
|
|
1960
|
-
return [TextContent(
|
|
1961
|
-
type="text",
|
|
1962
|
-
text=f"❌ Source is not ready for search. Status: {source_status}"
|
|
1963
|
-
)]
|
|
1964
|
-
except Exception as e:
|
|
1965
|
-
logger.warning(f"Could not check source status: {e}")
|
|
1966
|
-
# Continue anyway in case it's just a status check issue
|
|
1967
|
-
|
|
1968
|
-
# Perform the search
|
|
1969
|
-
logger.info(f"Searching local filesystem source {source_id} with query: {query}")
|
|
1970
|
-
|
|
1971
|
-
# Use the unified query endpoint with data_sources parameter
|
|
1972
|
-
result = client.query_unified(
|
|
1973
|
-
messages=[{"role": "user", "content": query}],
|
|
1974
|
-
data_sources=[source_id],
|
|
1975
|
-
include_sources=include_sources,
|
|
1976
|
-
stream=False
|
|
2785
|
+
logger.info(f"Reading file from {package_name} ({registry}): sha256={filename_sha256}, lines {start_line}-{end_line}")
|
|
2786
|
+
|
|
2787
|
+
# Read file content through backend
|
|
2788
|
+
result = await client.package_search_read_file(
|
|
2789
|
+
registry=registry,
|
|
2790
|
+
package_name=package_name,
|
|
2791
|
+
filename_sha256=filename_sha256,
|
|
2792
|
+
start_line=start_line,
|
|
2793
|
+
end_line=end_line,
|
|
2794
|
+
version=version
|
|
1977
2795
|
)
|
|
1978
|
-
|
|
1979
|
-
#
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
# Create our own formatted response
|
|
1996
|
-
formatted_response = f"🔍 **Search Results for Local Directory**\n"
|
|
1997
|
-
formatted_response += f"🔎 Query: \"{query}\"\n\n"
|
|
1998
|
-
formatted_response += response_text
|
|
1999
|
-
|
|
2000
|
-
# Add sources if available and requested
|
|
2001
|
-
if include_sources and sources:
|
|
2002
|
-
formatted_response += "\n\n**📄 Source Details:**\n"
|
|
2003
|
-
for i, source in enumerate(sources[:5], 1):
|
|
2004
|
-
metadata = source.get("metadata", {})
|
|
2005
|
-
file_path = metadata.get("file_path", "Unknown file")
|
|
2006
|
-
formatted_response += f"\n{i}. `{file_path}`\n"
|
|
2007
|
-
|
|
2008
|
-
# Add snippet of content
|
|
2009
|
-
content = source.get("content", "")
|
|
2010
|
-
if content:
|
|
2011
|
-
# Truncate to reasonable length
|
|
2012
|
-
lines = content.split('\n')[:10]
|
|
2013
|
-
snippet = '\n'.join(lines)
|
|
2014
|
-
if len(lines) > 10:
|
|
2015
|
-
snippet += "\n..."
|
|
2016
|
-
formatted_response += f"```\n{snippet}\n```\n"
|
|
2017
|
-
|
|
2018
|
-
return [TextContent(type="text", text=formatted_response)]
|
|
2796
|
+
|
|
2797
|
+
# Handle raw Chroma response (read_file typically returns content directly)
|
|
2798
|
+
response_lines = [
|
|
2799
|
+
f"# 📄 Package File Content: {package_name} ({registry})",
|
|
2800
|
+
f"**File SHA256:** `{filename_sha256}`",
|
|
2801
|
+
f"**Lines:** {start_line}-{end_line}"
|
|
2802
|
+
]
|
|
2803
|
+
|
|
2804
|
+
if version:
|
|
2805
|
+
response_lines.append(f"**Version:** {version}")
|
|
2806
|
+
|
|
2807
|
+
response_lines.append("\n```")
|
|
2808
|
+
# For read_file, Chroma typically returns the content directly as a string
|
|
2809
|
+
if isinstance(result, str):
|
|
2810
|
+
response_lines.append(result)
|
|
2811
|
+
elif isinstance(result, dict) and result.get("content"):
|
|
2812
|
+
response_lines.append(result["content"])
|
|
2019
2813
|
else:
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
except APIError as e:
|
|
2026
|
-
logger.error(f"API error searching local filesystem: {e}")
|
|
2027
|
-
return [TextContent(
|
|
2028
|
-
type="text",
|
|
2029
|
-
text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
|
|
2030
|
-
)]
|
|
2814
|
+
response_lines.append(str(result))
|
|
2815
|
+
response_lines.append("```")
|
|
2816
|
+
|
|
2817
|
+
return [TextContent(type="text", text="\n".join(response_lines))]
|
|
2818
|
+
|
|
2031
2819
|
except Exception as e:
|
|
2032
|
-
logger.error(f"
|
|
2820
|
+
logger.error(f"Error reading package file: {e}")
|
|
2033
2821
|
return [TextContent(
|
|
2034
2822
|
type="text",
|
|
2035
|
-
text=f"❌ Error
|
|
2823
|
+
text=f"❌ Error reading file: {str(e)}\n\n"
|
|
2824
|
+
f"Make sure:\n"
|
|
2825
|
+
f"- The registry is one of: crates_io, golang_proxy, npm, py_pi\n"
|
|
2826
|
+
f"- The package name is correct\n"
|
|
2827
|
+
f"- The filename_sha256 is valid\n"
|
|
2828
|
+
f"- The line range is valid (1-based, max 200 lines)"
|
|
2036
2829
|
)]
|
|
2037
2830
|
|
|
2038
2831
|
@mcp.tool()
|