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/server.py CHANGED
@@ -186,34 +186,22 @@ async def search_codebase(
186
186
  try:
187
187
  client = await ensure_api_client()
188
188
 
189
- # Get all indexed repositories if not specified
189
+ # Require explicit repository selection
190
190
  if not repositories:
191
- all_repos = await client.list_repositories()
192
-
193
- # Ensure all_repos is a list and contains dictionaries
194
- if not isinstance(all_repos, list):
195
- logger.error(f"Unexpected type for all_repos: {type(all_repos)}")
196
- return [TextContent(
197
- type="text",
198
- text=" Error retrieving repositories. The API returned an unexpected response."
199
- )]
200
-
201
- repositories = []
202
- for repo in all_repos:
203
- if isinstance(repo, dict) and repo.get("status") == "completed":
204
- repo_name = repo.get("repository")
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 source IDs to search. Use it based on user's query.
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
- - Always use Source ID. If you don't have it, use `list_documentation` tool to get it.
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
- # Get all indexed documentation sources if not specified
354
+ # Require explicit source selection
363
355
  if not sources:
364
- all_sources = await client.list_data_sources()
365
- sources = [source["id"] for source in all_sources if source.get("status") == "completed"]
366
- if not sources:
367
- return [TextContent(
368
- type="text",
369
- text=" No indexed documentation found. Use `index_documentation` to index documentation first."
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 delete_documentation(source_id: str) -> List[TextContent]:
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 index_local_filesystem(
1638
- directory_path: str,
1639
- inclusion_patterns: Optional[List[str]] = None,
1640
- exclusion_patterns: Optional[List[str]] = None,
1641
- max_file_size_mb: int = 50
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
- Index a local filesystem directory for intelligent search.
1645
-
1646
- Args:
1647
- directory_path: Absolute path to the directory to index
1648
- inclusion_patterns: Optional list of patterns to include (e.g., ["ext:.py", "dir:src"])
1649
- exclusion_patterns: Optional list of patterns to exclude (e.g., ["dir:node_modules", "ext:.log"])
1650
- max_file_size_mb: Maximum file size in MB to process (default: 50)
1651
-
1652
- Returns:
1653
- Status of the indexing operation
1654
-
1655
- Important:
1656
- - Path must be absolute (e.g., /Users/username/projects/myproject)
1657
- - When indexing starts, use check_local_filesystem_status tool to monitor progress
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
- # Validate absolute path
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
- # Start indexing
1671
- logger.info(f"Starting to index local directory: {directory_path}")
1672
- result = await client.index_local_filesystem(
1673
- directory_path=directory_path,
1674
- inclusion_patterns=inclusion_patterns or [],
1675
- exclusion_patterns=exclusion_patterns or [],
1676
- max_file_size_mb=max_file_size_mb
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
- if result.get("success"):
1680
- source_id = result["data"]["source_id"]
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
- else:
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" Failed to start indexing: {result.get('detail', 'Unknown error')}"
2533
+ text=f"No matches found for pattern '{pattern}' in {package_name} ({registry})"
1702
2534
  )]
1703
-
1704
- except APIError as e:
1705
- logger.error(f"API error indexing local filesystem: {e}")
1706
- return [TextContent(
1707
- type="text",
1708
- text=f"❌ API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
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"Unexpected error indexing local filesystem: {e}")
2588
+ logger.error(f"Error in package search grep: {e}")
1712
2589
  return [TextContent(
1713
2590
  type="text",
1714
- text=f"❌ Error: An unexpected error occurred while indexing the directory: {str(e)}"
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 scan_local_filesystem(
1719
- directory_path: str,
1720
- inclusion_patterns: Optional[List[str]] = None,
1721
- exclusion_patterns: Optional[List[str]] = None,
1722
- max_file_size_mb: int = 50
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
- Scan a local filesystem directory to preview what files would be indexed.
1726
-
1727
- This tool helps you understand what files will be processed before actually indexing.
1728
-
1729
- Args:
1730
- directory_path: Absolute path to the directory to scan
1731
- inclusion_patterns: Optional list of patterns to include (e.g., ["ext:.py", "dir:src"])
1732
- exclusion_patterns: Optional list of patterns to exclude (e.g., ["dir:node_modules", "ext:.log"])
1733
- max_file_size_mb: Maximum file size in MB to process (default: 50)
1734
-
1735
- Returns:
1736
- Summary of files that would be indexed including count, size, and file types
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
- # Validate absolute path
1740
- if not os.path.isabs(directory_path):
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" Error: directory_path must be an absolute path. Got: {directory_path}\n\n"
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
- @mcp.tool()
1811
- async def check_local_filesystem_status(source_id: str) -> List[TextContent]:
1812
- """
1813
- Check the indexing status of a local filesystem source.
1814
-
1815
- Args:
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" Source ID `{source_id}` not found. Please check the ID and try again."
2669
+ text=f"No relevant code found for queries:\n{queries_str}\n\nin {package_name} ({registry})"
1876
2670
  )]
1877
- return [TextContent(
1878
- type="text",
1879
- text=f" API Error: {str(e)}\n\nStatus Code: {e.status_code}\nDetails: {e.detail}"
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"Unexpected error checking local filesystem status: {e}")
2727
+ logger.error(f"Error in package search hybrid: {e}")
1883
2728
  return [TextContent(
1884
2729
  type="text",
1885
- text=f"❌ Error: An unexpected error occurred: {str(e)}"
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 search_local_filesystem(
1890
- source_id: str,
1891
- query: str,
1892
- include_sources: bool = True
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
- Search an indexed local filesystem directory using its source ID.
1896
-
1897
- To search local files:
1898
- 1. First index a directory using `index_local_filesystem` - this will return a source_id
1899
- 2. Use that source_id with this tool to search the indexed content
1900
-
1901
- Args:
1902
- source_id: The source ID returned when the directory was indexed (required)
1903
- query: Your search query in natural language (required)
1904
- include_sources: Whether to include source code snippets in results (default: True)
1905
-
1906
- Returns:
1907
- Search results with relevant file snippets and explanations
1908
-
1909
- Example:
1910
- # After indexing returns source_id "abc123-def456"
1911
- search_local_filesystem(
1912
- source_id="abc123-def456",
1913
- query="configuration settings"
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 inputs
1921
- if not source_id:
2770
+ # Validate line range
2771
+ if end_line - start_line + 1 > 200:
1922
2772
  return [TextContent(
1923
2773
  type="text",
1924
- text="❌ Error: 'source_id' parameter is required. Use the ID returned from index_local_filesystem."
2774
+ text="❌ Error: Maximum 200 lines can be read at once. Please reduce the line range."
1925
2775
  )]
1926
-
1927
- if not query:
2776
+
2777
+ if start_line < 1 or end_line < start_line:
1928
2778
  return [TextContent(
1929
2779
  type="text",
1930
- text="❌ Error: 'query' parameter is required"
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
- # Check if the source exists and is ready
1936
- logger.info(f"Checking status of source {source_id}")
1937
- try:
1938
- status = await client.get_data_source_status(source_id)
1939
- if not status:
1940
- return [TextContent(
1941
- type="text",
1942
- text=f"❌ Source ID '{source_id}' not found. Please check the ID and try again."
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
- # Parse the response
1980
- response_text = ""
1981
- async for chunk in result:
1982
- data = json.loads(chunk)
1983
- if "content" in data:
1984
- response_text = data["content"]
1985
- sources = data.get("sources", [])
1986
- break
1987
-
1988
- # Format the response nicely for local filesystem results
1989
- if response_text:
1990
- # Extract the local filesystem results section if present
1991
- if "**Local filesystem results" in response_text:
1992
- # Keep the original response
1993
- formatted_response = response_text
1994
- else:
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
- return [TextContent(
2021
- type="text",
2022
- text=f"No results found for query: \"{query}\" in the indexed directory."
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"Unexpected error searching local filesystem: {e}")
2820
+ logger.error(f"Error reading package file: {e}")
2033
2821
  return [TextContent(
2034
2822
  type="text",
2035
- text=f"❌ Error: An unexpected error occurred: {str(e)}"
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()