nia-mcp-server 1.0.22__tar.gz → 1.0.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nia-mcp-server might be problematic. Click here for more details.

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