nia-mcp-server 1.0.22__tar.gz → 1.0.23__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.23}/PKG-INFO +1 -1
  2. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/pyproject.toml +1 -1
  3. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/__init__.py +2 -1
  4. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/api_client.py +165 -2
  5. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/server.py +589 -20
  6. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/.gitignore +0 -0
  7. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/ARCHITECTURE.md +0 -0
  8. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/LICENSE +0 -0
  9. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/README.md +0 -0
  10. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/nia_analytics.log +0 -0
  11. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/nia_mcp_server.log +0 -0
  12. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/__main__.py +0 -0
  13. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/assets/rules/claude_rules.md +0 -0
  14. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/assets/rules/cursor_rules.md +0 -0
  15. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/assets/rules/nia_rules.md +0 -0
  16. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/assets/rules/vscode_rules.md +0 -0
  17. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/assets/rules/windsurf_rules.md +0 -0
  18. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/cli.py +0 -0
  19. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/profiles.py +0 -0
  20. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/project_init.py +0 -0
  21. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/src/nia_mcp_server/rule_transformer.py +0 -0
  22. {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.23}/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.23
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.23"
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.23"
@@ -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.23",
32
32
  "Content-Type": "application/json"
33
33
  },
34
34
  timeout=720.0 # 12 minute timeout for deep research operations
@@ -913,4 +913,167 @@ 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
+ ) -> Dict[str, Any]:
931
+ """Save a conversation context for cross-agent sharing."""
932
+ try:
933
+ payload = {
934
+ "title": title,
935
+ "summary": summary,
936
+ "content": content,
937
+ "agent_source": agent_source,
938
+ "tags": tags or [],
939
+ "metadata": metadata or {}
940
+ }
941
+
942
+ response = await self.client.post(
943
+ f"{self.base_url}/v2/contexts",
944
+ json=payload
945
+ )
946
+ response.raise_for_status()
947
+ return response.json()
948
+
949
+ except httpx.HTTPStatusError as e:
950
+ raise self._handle_api_error(e)
951
+ except Exception as e:
952
+ raise APIError(f"Failed to save context: {str(e)}")
953
+
954
+ async def list_contexts(
955
+ self,
956
+ limit: int = 20,
957
+ offset: int = 0,
958
+ tags: Optional[str] = None,
959
+ agent_source: Optional[str] = None
960
+ ) -> Dict[str, Any]:
961
+ """List user's conversation contexts with pagination and filtering."""
962
+ try:
963
+ params = {
964
+ "limit": limit,
965
+ "offset": offset
966
+ }
967
+
968
+ if tags:
969
+ params["tags"] = tags
970
+ if agent_source:
971
+ params["agent_source"] = agent_source
972
+
973
+ response = await self.client.get(
974
+ f"{self.base_url}/v2/contexts",
975
+ params=params
976
+ )
977
+ response.raise_for_status()
978
+ return response.json()
979
+
980
+ except httpx.HTTPStatusError as e:
981
+ raise self._handle_api_error(e)
982
+ except Exception as e:
983
+ raise APIError(f"Failed to list contexts: {str(e)}")
984
+
985
+ async def get_context(self, context_id: str) -> Dict[str, Any]:
986
+ """Get a specific conversation context by ID."""
987
+ try:
988
+ response = await self.client.get(f"{self.base_url}/v2/contexts/{context_id}")
989
+ response.raise_for_status()
990
+ return response.json()
991
+
992
+ except httpx.HTTPStatusError as e:
993
+ if e.response.status_code == 404:
994
+ return None
995
+ raise self._handle_api_error(e)
996
+ except Exception as e:
997
+ raise APIError(f"Failed to get context: {str(e)}")
998
+
999
+ async def update_context(
1000
+ self,
1001
+ context_id: str,
1002
+ title: Optional[str] = None,
1003
+ summary: Optional[str] = None,
1004
+ content: Optional[str] = None,
1005
+ tags: Optional[List[str]] = None,
1006
+ metadata: Optional[Dict[str, Any]] = None
1007
+ ) -> Dict[str, Any]:
1008
+ """Update an existing conversation context."""
1009
+ try:
1010
+ payload = {}
1011
+
1012
+ if title is not None:
1013
+ payload["title"] = title
1014
+ if summary is not None:
1015
+ payload["summary"] = summary
1016
+ if content is not None:
1017
+ payload["content"] = content
1018
+ if tags is not None:
1019
+ payload["tags"] = tags
1020
+ if metadata is not None:
1021
+ payload["metadata"] = metadata
1022
+
1023
+ response = await self.client.put(
1024
+ f"{self.base_url}/v2/contexts/{context_id}",
1025
+ json=payload
1026
+ )
1027
+ response.raise_for_status()
1028
+ return response.json()
1029
+
1030
+ except httpx.HTTPStatusError as e:
1031
+ raise self._handle_api_error(e)
1032
+ except Exception as e:
1033
+ raise APIError(f"Failed to update context: {str(e)}")
1034
+
1035
+ async def delete_context(self, context_id: str) -> bool:
1036
+ """Delete a conversation context."""
1037
+ try:
1038
+ response = await self.client.delete(f"{self.base_url}/v2/contexts/{context_id}")
1039
+ response.raise_for_status()
1040
+ return True
1041
+
1042
+ except httpx.HTTPStatusError as e:
1043
+ if e.response.status_code == 404:
1044
+ return False
1045
+ raise self._handle_api_error(e)
1046
+ except Exception as e:
1047
+ logger.error(f"Failed to delete context: {e}")
1048
+ return False
1049
+
1050
+ async def search_contexts(
1051
+ self,
1052
+ query: str,
1053
+ limit: int = 20,
1054
+ tags: Optional[str] = None,
1055
+ agent_source: Optional[str] = None
1056
+ ) -> Dict[str, Any]:
1057
+ """Search conversation contexts by content, title, or summary."""
1058
+ try:
1059
+ params = {
1060
+ "q": query,
1061
+ "limit": limit
1062
+ }
1063
+
1064
+ if tags:
1065
+ params["tags"] = tags
1066
+ if agent_source:
1067
+ params["agent_source"] = agent_source
1068
+
1069
+ response = await self.client.get(
1070
+ f"{self.base_url}/v2/contexts/search",
1071
+ params=params
1072
+ )
1073
+ response.raise_for_status()
1074
+ return response.json()
1075
+
1076
+ except httpx.HTTPStatusError as e:
1077
+ raise self._handle_api_error(e)
1078
+ except Exception as e:
1079
+ 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,7 +2826,7 @@ 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()
2829
+ # @mcp.tool()
2832
2830
  async def visualize_codebase(
2833
2831
  repository: str
2834
2832
  ) -> List[TextContent]:
@@ -3087,6 +3085,577 @@ async def nia_bug_report(
3087
3085
  )
3088
3086
  ]
3089
3087
 
3088
+ # Context Sharing Tools
3089
+
3090
+ @mcp.tool()
3091
+ async def save_context(
3092
+ title: str,
3093
+ summary: str,
3094
+ content: str,
3095
+ agent_source: str,
3096
+ tags: Optional[List[str]] = None,
3097
+ metadata: Optional[dict] = None
3098
+ ) -> List[TextContent]:
3099
+ """
3100
+ Save a conversation context for cross-agent sharing.
3101
+
3102
+ This tool enables agents to save conversation contexts that can be shared
3103
+ with other AI agents, creating seamless handoffs between different coding
3104
+ environments (e.g., Cursor → Claude Code).
3105
+
3106
+ Args:
3107
+ title: A descriptive title for the context (1-200 characters)
3108
+ summary: Brief summary of the conversation (10-1000 characters)
3109
+ content: Full conversation context - the agent should compact the conversation history but keep all important parts togethers, as well as code snippets. No excuses.
3110
+ agent_source: Which agent is creating this context (e.g., "cursor", "claude-code", "windsurf")
3111
+ tags: Optional list of searchable tags (up to 10 tags)
3112
+ metadata: Optional metadata like file paths, repositories discussed, etc.
3113
+
3114
+ Returns:
3115
+ Confirmation of successful context save with context ID
3116
+
3117
+ Example:
3118
+ save_context(
3119
+ title="Streaming AI SDK Implementation",
3120
+ summary="Planning conversation about implementing streaming responses with AI SDK",
3121
+ content="User asked about implementing streaming... [agent should include conversation]",
3122
+ agent_source="cursor",
3123
+ tags=["streaming", "ai-sdk", "implementation"],
3124
+ metadata={"files": ["src/api/chat.ts"], "repos": ["myproject"]}
3125
+ )
3126
+ """
3127
+ try:
3128
+ # Validate input parameters
3129
+ if not title or not title.strip():
3130
+ return [TextContent(type="text", text="❌ Error: Title is required")]
3131
+
3132
+ if len(title) > 200:
3133
+ return [TextContent(type="text", text="❌ Error: Title must be 200 characters or less")]
3134
+
3135
+ if not summary or len(summary) < 10:
3136
+ return [TextContent(type="text", text="❌ Error: Summary must be at least 10 characters")]
3137
+
3138
+ if len(summary) > 1000:
3139
+ return [TextContent(type="text", text="❌ Error: Summary must be 1000 characters or less")]
3140
+
3141
+ if not content or len(content) < 50:
3142
+ return [TextContent(type="text", text="❌ Error: Content must be at least 50 characters")]
3143
+
3144
+ if not agent_source or not agent_source.strip():
3145
+ return [TextContent(type="text", text="❌ Error: Agent source is required")]
3146
+
3147
+ if tags and len(tags) > 10:
3148
+ return [TextContent(type="text", text="❌ Error: Maximum 10 tags allowed")]
3149
+
3150
+ client = await ensure_api_client()
3151
+
3152
+ logger.info(f"Saving context: title='{title}', agent={agent_source}, content_length={len(content)}")
3153
+
3154
+ result = await client.save_context(
3155
+ title=title.strip(),
3156
+ summary=summary.strip(),
3157
+ content=content,
3158
+ agent_source=agent_source.strip(),
3159
+ tags=tags or [],
3160
+ metadata=metadata or {}
3161
+ )
3162
+
3163
+ context_id = result.get("id")
3164
+
3165
+ return [TextContent(
3166
+ type="text",
3167
+ text=f"✅ **Context Saved Successfully!**\n\n"
3168
+ f"🆔 **Context ID:** `{context_id}`\n"
3169
+ f"📝 **Title:** {title}\n"
3170
+ f"🤖 **Source Agent:** {agent_source}\n"
3171
+ f"📊 **Content Length:** {len(content):,} characters\n"
3172
+ f"🏷️ **Tags:** {', '.join(tags) if tags else 'None'}\n\n"
3173
+ f"**Next Steps:**\n"
3174
+ f"• Other agents can now retrieve this context using the context ID\n"
3175
+ f"• Use `search_contexts` to find contexts by content or tags\n"
3176
+ f"• Use `list_contexts` to see all your saved contexts\n\n"
3177
+ f"🔗 **Share this context:** Provide the context ID `{context_id}` to other agents"
3178
+ )]
3179
+
3180
+ except APIError as e:
3181
+ logger.error(f"API Error saving context: {e}")
3182
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3183
+ except Exception as e:
3184
+ logger.error(f"Error saving context: {e}")
3185
+ return [TextContent(type="text", text=f"❌ Error saving context: {str(e)}")]
3186
+
3187
+ @mcp.tool()
3188
+ async def list_contexts(
3189
+ limit: int = 20,
3190
+ offset: int = 0,
3191
+ tags: Optional[str] = None,
3192
+ agent_source: Optional[str] = None
3193
+ ) -> List[TextContent]:
3194
+ """
3195
+ List saved conversation contexts with pagination and filtering.
3196
+
3197
+ Args:
3198
+ limit: Number of contexts to return (1-100, default: 20)
3199
+ offset: Number of contexts to skip for pagination (default: 0)
3200
+ tags: Comma-separated tags to filter by (optional)
3201
+ agent_source: Filter by specific agent source (optional)
3202
+
3203
+ Returns:
3204
+ List of conversation contexts with pagination info
3205
+
3206
+ Examples:
3207
+ - list_contexts() - List recent 20 contexts
3208
+ - list_contexts(limit=50) - List recent 50 contexts
3209
+ - list_contexts(tags="streaming,ai-sdk") - Filter by tags
3210
+ - list_contexts(agent_source="cursor") - Only contexts from Cursor
3211
+ """
3212
+ try:
3213
+ # Validate parameters
3214
+ if limit < 1 or limit > 100:
3215
+ return [TextContent(type="text", text="❌ Error: Limit must be between 1 and 100")]
3216
+
3217
+ if offset < 0:
3218
+ return [TextContent(type="text", text="❌ Error: Offset must be 0 or greater")]
3219
+
3220
+ client = await ensure_api_client()
3221
+
3222
+ result = await client.list_contexts(
3223
+ limit=limit,
3224
+ offset=offset,
3225
+ tags=tags,
3226
+ agent_source=agent_source
3227
+ )
3228
+
3229
+ contexts = result.get("contexts", [])
3230
+ pagination = result.get("pagination", {})
3231
+
3232
+ if not contexts:
3233
+ response = "📭 **No Contexts Found**\n\n"
3234
+ if tags or agent_source:
3235
+ response += "No contexts match your filters.\n\n"
3236
+ else:
3237
+ response += "You haven't saved any contexts yet.\n\n"
3238
+
3239
+ response += "**Get started:**\n"
3240
+ response += "• Use `save_context` to save a conversation for cross-agent sharing\n"
3241
+ response += "• Perfect for handoffs between Cursor and Claude Code!"
3242
+
3243
+ return [TextContent(type="text", text=response)]
3244
+
3245
+ # Format the response
3246
+ response = f"📚 **Your Conversation Contexts** ({pagination.get('total', len(contexts))} total)\n\n"
3247
+
3248
+ for i, context in enumerate(contexts, offset + 1):
3249
+ created_at = context.get('created_at', '')
3250
+ if created_at:
3251
+ # Format datetime for better readability
3252
+ try:
3253
+ from datetime import datetime
3254
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
3255
+ formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
3256
+ except:
3257
+ formatted_date = created_at
3258
+ else:
3259
+ formatted_date = 'Unknown'
3260
+
3261
+ response += f"**{i}. {context['title']}**\n"
3262
+ response += f" 🆔 ID: `{context['id']}`\n"
3263
+ response += f" 🤖 Source: {context['agent_source']}\n"
3264
+ response += f" 📅 Created: {formatted_date}\n"
3265
+ response += f" 📝 Summary: {context['summary'][:100]}{'...' if len(context['summary']) > 100 else ''}\n"
3266
+ if context.get('tags'):
3267
+ response += f" 🏷️ Tags: {', '.join(context['tags'])}\n"
3268
+ response += "\n"
3269
+
3270
+ # Add pagination info
3271
+ if pagination.get('has_more'):
3272
+ next_offset = offset + limit
3273
+ response += f"📄 **Pagination:** Showing {offset + 1}-{offset + len(contexts)} of {pagination.get('total')}\n"
3274
+ response += f" Use `list_contexts(offset={next_offset})` for next page\n"
3275
+
3276
+ response += "\n**Actions:**\n"
3277
+ response += "• `retrieve_context(context_id)` - Get full context\n"
3278
+ response += "• `search_contexts(query)` - Search contexts\n"
3279
+ response += "• `delete_context(context_id)` - Remove context"
3280
+
3281
+ return [TextContent(type="text", text=response)]
3282
+
3283
+ except APIError as e:
3284
+ logger.error(f"API Error listing contexts: {e}")
3285
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3286
+ except Exception as e:
3287
+ logger.error(f"Error listing contexts: {e}")
3288
+ return [TextContent(type="text", text=f"❌ Error listing contexts: {str(e)}")]
3289
+
3290
+ @mcp.tool()
3291
+ async def retrieve_context(context_id: str) -> List[TextContent]:
3292
+ """
3293
+ Retrieve a specific conversation context by ID.
3294
+
3295
+ Use this tool to get the full conversation context that was saved by
3296
+ another agent. Perfect for getting strategic context from Cursor
3297
+ when working in Claude Code.
3298
+
3299
+ Args:
3300
+ context_id: The unique ID of the context to retrieve
3301
+
3302
+ Returns:
3303
+ Full conversation context with metadata
3304
+
3305
+ Example:
3306
+ retrieve_context("550e8400-e29b-41d4-a716-446655440000")
3307
+ """
3308
+ try:
3309
+ if not context_id or not context_id.strip():
3310
+ return [TextContent(type="text", text="❌ Error: Context ID is required")]
3311
+
3312
+ client = await ensure_api_client()
3313
+
3314
+ context = await client.get_context(context_id.strip())
3315
+
3316
+ if not context:
3317
+ return [TextContent(
3318
+ type="text",
3319
+ text=f"❌ **Context Not Found**\n\n"
3320
+ f"Context ID `{context_id}` was not found.\n\n"
3321
+ f"**Possible reasons:**\n"
3322
+ f"• The context ID is incorrect\n"
3323
+ f"• The context belongs to a different user\n"
3324
+ f"• The context has been deleted\n\n"
3325
+ f"Use `list_contexts()` to see your available contexts."
3326
+ )]
3327
+
3328
+ # Format the context display
3329
+ created_at = context.get('created_at', '')
3330
+ if created_at:
3331
+ try:
3332
+ from datetime import datetime
3333
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
3334
+ formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
3335
+ except:
3336
+ formatted_date = created_at
3337
+ else:
3338
+ formatted_date = 'Unknown'
3339
+
3340
+ updated_at = context.get('updated_at', '')
3341
+ if updated_at:
3342
+ try:
3343
+ from datetime import datetime
3344
+ dt = datetime.fromisoformat(updated_at.replace('Z', '+00:00'))
3345
+ formatted_updated = dt.strftime('%Y-%m-%d %H:%M UTC')
3346
+ except:
3347
+ formatted_updated = updated_at
3348
+ else:
3349
+ formatted_updated = None
3350
+
3351
+ response = f"📋 **Context: {context['title']}**\n\n"
3352
+ response += f"🆔 **ID:** `{context['id']}`\n"
3353
+ response += f"🤖 **Source Agent:** {context['agent_source']}\n"
3354
+ response += f"📅 **Created:** {formatted_date}\n"
3355
+ if formatted_updated:
3356
+ response += f"🔄 **Updated:** {formatted_updated}\n"
3357
+
3358
+ if context.get('tags'):
3359
+ response += f"🏷️ **Tags:** {', '.join(context['tags'])}\n"
3360
+
3361
+ response += f"\n📝 **Summary:**\n{context['summary']}\n\n"
3362
+
3363
+ # Add metadata if available
3364
+ metadata = context.get('metadata', {})
3365
+ if metadata:
3366
+ response += f"📊 **Metadata:**\n"
3367
+ for key, value in metadata.items():
3368
+ if isinstance(value, list):
3369
+ response += f"• **{key}:** {', '.join(map(str, value))}\n"
3370
+ else:
3371
+ response += f"• **{key}:** {value}\n"
3372
+ response += "\n"
3373
+
3374
+ response += f"📄 **Full Context:**\n\n{context['content']}\n\n"
3375
+
3376
+ response += f"---\n"
3377
+ response += f"💡 **Tips:**\n"
3378
+ response += f"• This context was created by {context['agent_source']}\n"
3379
+ response += f"• Use this information to understand the strategic planning\n"
3380
+ response += f"• You can now implement based on this context!"
3381
+
3382
+ return [TextContent(type="text", text=response)]
3383
+
3384
+ except APIError as e:
3385
+ logger.error(f"API Error retrieving context: {e}")
3386
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3387
+ except Exception as e:
3388
+ logger.error(f"Error retrieving context: {e}")
3389
+ return [TextContent(type="text", text=f"❌ Error retrieving context: {str(e)}")]
3390
+
3391
+ @mcp.tool()
3392
+ async def search_contexts(
3393
+ query: str,
3394
+ limit: int = 20,
3395
+ tags: Optional[str] = None,
3396
+ agent_source: Optional[str] = None
3397
+ ) -> List[TextContent]:
3398
+ """
3399
+ Search conversation contexts by content, title, or summary.
3400
+
3401
+ Perfect for finding relevant contexts when you remember part of the
3402
+ conversation but not the exact context ID.
3403
+
3404
+ Args:
3405
+ query: Search query to match against title, summary, content, and tags
3406
+ limit: Maximum number of results to return (1-100, default: 20)
3407
+ tags: Comma-separated tags to filter by (optional)
3408
+ agent_source: Filter by specific agent source (optional)
3409
+
3410
+ Returns:
3411
+ Search results with matching contexts
3412
+
3413
+ Examples:
3414
+ - search_contexts("streaming AI SDK")
3415
+ - search_contexts("authentication", tags="security,implementation")
3416
+ - search_contexts("database", agent_source="cursor")
3417
+ """
3418
+ try:
3419
+ # Validate parameters
3420
+ if not query or not query.strip():
3421
+ return [TextContent(type="text", text="❌ Error: Search query is required")]
3422
+
3423
+ if limit < 1 or limit > 100:
3424
+ return [TextContent(type="text", text="❌ Error: Limit must be between 1 and 100")]
3425
+
3426
+ client = await ensure_api_client()
3427
+
3428
+ result = await client.search_contexts(
3429
+ query=query.strip(),
3430
+ limit=limit,
3431
+ tags=tags,
3432
+ agent_source=agent_source
3433
+ )
3434
+
3435
+ contexts = result.get("contexts", [])
3436
+
3437
+ if not contexts:
3438
+ response = f"🔍 **No Results Found**\n\n"
3439
+ response += f"No contexts match your search query: \"{query}\"\n\n"
3440
+
3441
+ if tags or agent_source:
3442
+ response += f"**Active filters:**\n"
3443
+ if tags:
3444
+ response += f"• Tags: {tags}\n"
3445
+ if agent_source:
3446
+ response += f"• Agent: {agent_source}\n"
3447
+ response += "\n"
3448
+
3449
+ response += f"**Suggestions:**\n"
3450
+ response += f"• Try different keywords\n"
3451
+ response += f"• Remove filters to broaden search\n"
3452
+ response += f"• Use `list_contexts()` to see all contexts"
3453
+
3454
+ return [TextContent(type="text", text=response)]
3455
+
3456
+ # Format search results
3457
+ response = f"🔍 **Search Results for \"{query}\"** ({len(contexts)} found)\n\n"
3458
+
3459
+ for i, context in enumerate(contexts, 1):
3460
+ created_at = context.get('created_at', '')
3461
+ if created_at:
3462
+ try:
3463
+ from datetime import datetime
3464
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
3465
+ formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
3466
+ except:
3467
+ formatted_date = created_at
3468
+ else:
3469
+ formatted_date = 'Unknown'
3470
+
3471
+ response += f"**{i}. {context['title']}**\n"
3472
+ response += f" 🆔 ID: `{context['id']}`\n"
3473
+ response += f" 🤖 Source: {context['agent_source']}\n"
3474
+ response += f" 📅 Created: {formatted_date}\n"
3475
+ response += f" 📝 Summary: {context['summary'][:150]}{'...' if len(context['summary']) > 150 else ''}\n"
3476
+
3477
+ if context.get('tags'):
3478
+ response += f" 🏷️ Tags: {', '.join(context['tags'])}\n"
3479
+
3480
+ response += "\n"
3481
+
3482
+ response += f"**Actions:**\n"
3483
+ response += f"• `retrieve_context(context_id)` - Get full context\n"
3484
+ response += f"• Refine search with different keywords\n"
3485
+ response += f"• Use tags or agent filters for better results"
3486
+
3487
+ return [TextContent(type="text", text=response)]
3488
+
3489
+ except APIError as e:
3490
+ logger.error(f"API Error searching contexts: {e}")
3491
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3492
+ except Exception as e:
3493
+ logger.error(f"Error searching contexts: {e}")
3494
+ return [TextContent(type="text", text=f"❌ Error searching contexts: {str(e)}")]
3495
+
3496
+ @mcp.tool()
3497
+ async def update_context(
3498
+ context_id: str,
3499
+ title: Optional[str] = None,
3500
+ summary: Optional[str] = None,
3501
+ content: Optional[str] = None,
3502
+ tags: Optional[List[str]] = None,
3503
+ metadata: Optional[dict] = None
3504
+ ) -> List[TextContent]:
3505
+ """
3506
+ Update an existing conversation context.
3507
+
3508
+ Args:
3509
+ context_id: The unique ID of the context to update
3510
+ title: Updated title (optional)
3511
+ summary: Updated summary (optional)
3512
+ content: Updated content (optional)
3513
+ tags: Updated tags list (optional)
3514
+ metadata: Updated metadata (optional)
3515
+
3516
+ Returns:
3517
+ Confirmation of successful update
3518
+
3519
+ Example:
3520
+ update_context(
3521
+ context_id="550e8400-e29b-41d4-a716-446655440000",
3522
+ title="Updated: Streaming AI SDK Implementation",
3523
+ tags=["streaming", "ai-sdk", "completed"]
3524
+ )
3525
+ """
3526
+ try:
3527
+ if not context_id or not context_id.strip():
3528
+ return [TextContent(type="text", text="❌ Error: Context ID is required")]
3529
+
3530
+ # Check that at least one field is being updated
3531
+ if not any([title, summary, content, tags is not None, metadata is not None]):
3532
+ return [TextContent(
3533
+ type="text",
3534
+ text="❌ Error: At least one field must be provided for update"
3535
+ )]
3536
+
3537
+ # Validate fields if provided
3538
+ if title is not None and (not title.strip() or len(title) > 200):
3539
+ return [TextContent(
3540
+ type="text",
3541
+ text="❌ Error: Title must be 1-200 characters"
3542
+ )]
3543
+
3544
+ if summary is not None and (len(summary) < 10 or len(summary) > 1000):
3545
+ return [TextContent(
3546
+ type="text",
3547
+ text="❌ Error: Summary must be 10-1000 characters"
3548
+ )]
3549
+
3550
+ if content is not None and len(content) < 50:
3551
+ return [TextContent(
3552
+ type="text",
3553
+ text="❌ Error: Content must be at least 50 characters"
3554
+ )]
3555
+
3556
+ if tags is not None and len(tags) > 10:
3557
+ return [TextContent(
3558
+ type="text",
3559
+ text="❌ Error: Maximum 10 tags allowed"
3560
+ )]
3561
+
3562
+ client = await ensure_api_client()
3563
+
3564
+ result = await client.update_context(
3565
+ context_id=context_id.strip(),
3566
+ title=title.strip() if title else None,
3567
+ summary=summary.strip() if summary else None,
3568
+ content=content,
3569
+ tags=tags,
3570
+ metadata=metadata
3571
+ )
3572
+
3573
+ if not result:
3574
+ return [TextContent(
3575
+ type="text",
3576
+ text=f"❌ Error: Context with ID `{context_id}` not found"
3577
+ )]
3578
+
3579
+ # List updated fields
3580
+ updated_fields = []
3581
+ if title is not None:
3582
+ updated_fields.append("title")
3583
+ if summary is not None:
3584
+ updated_fields.append("summary")
3585
+ if content is not None:
3586
+ updated_fields.append("content")
3587
+ if tags is not None:
3588
+ updated_fields.append("tags")
3589
+ if metadata is not None:
3590
+ updated_fields.append("metadata")
3591
+
3592
+ response = f"✅ **Context Updated Successfully!**\n\n"
3593
+ response += f"🆔 **Context ID:** `{context_id}`\n"
3594
+ response += f"📝 **Title:** {result['title']}\n"
3595
+ response += f"🔄 **Updated Fields:** {', '.join(updated_fields)}\n"
3596
+ response += f"🤖 **Source Agent:** {result['agent_source']}\n\n"
3597
+
3598
+ response += f"**Current Status:**\n"
3599
+ response += f"• **Tags:** {', '.join(result['tags']) if result.get('tags') else 'None'}\n"
3600
+ response += f"• **Content Length:** {len(result['content']):,} characters\n\n"
3601
+
3602
+ response += f"Use `retrieve_context('{context_id}')` to see the full updated context."
3603
+
3604
+ return [TextContent(type="text", text=response)]
3605
+
3606
+ except APIError as e:
3607
+ logger.error(f"API Error updating context: {e}")
3608
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3609
+ except Exception as e:
3610
+ logger.error(f"Error updating context: {e}")
3611
+ return [TextContent(type="text", text=f"❌ Error updating context: {str(e)}")]
3612
+
3613
+ @mcp.tool()
3614
+ async def delete_context(context_id: str) -> List[TextContent]:
3615
+ """
3616
+ Delete a conversation context.
3617
+
3618
+ Args:
3619
+ context_id: The unique ID of the context to delete
3620
+
3621
+ Returns:
3622
+ Confirmation of successful deletion
3623
+
3624
+ Example:
3625
+ delete_context("550e8400-e29b-41d4-a716-446655440000")
3626
+ """
3627
+ try:
3628
+ if not context_id or not context_id.strip():
3629
+ return [TextContent(type="text", text="❌ Error: Context ID is required")]
3630
+
3631
+ client = await ensure_api_client()
3632
+
3633
+ success = await client.delete_context(context_id.strip())
3634
+
3635
+ if success:
3636
+ return [TextContent(
3637
+ type="text",
3638
+ text=f"✅ **Context Deleted Successfully!**\n\n"
3639
+ f"🆔 **Context ID:** `{context_id}`\n\n"
3640
+ f"The context has been permanently removed from your account.\n"
3641
+ f"This action cannot be undone.\n\n"
3642
+ f"Use `list_contexts()` to see your remaining contexts."
3643
+ )]
3644
+ else:
3645
+ return [TextContent(
3646
+ type="text",
3647
+ text=f"❌ **Context Not Found**\n\n"
3648
+ f"Context ID `{context_id}` was not found or has already been deleted.\n\n"
3649
+ f"Use `list_contexts()` to see your available contexts."
3650
+ )]
3651
+
3652
+ except APIError as e:
3653
+ logger.error(f"API Error deleting context: {e}")
3654
+ return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
3655
+ except Exception as e:
3656
+ logger.error(f"Error deleting context: {e}")
3657
+ return [TextContent(type="text", text=f"❌ Error deleting context: {str(e)}")]
3658
+
3090
3659
  # Resources
3091
3660
 
3092
3661
  # Note: FastMCP doesn't have list_resources or read_resource decorators
File without changes