From f48b86bafe96cec5fc8139f3d873e0423ea12610 Mon Sep 17 00:00:00 2001 From: Cem Karaca Date: Sat, 29 Nov 2025 08:09:41 -0500 Subject: [PATCH 1/5] feat: Add clipboard source tracking - Add ClipboardSource enum (ui/sdk/mcp) - Add source field to ClipboardEntry model - Hardcode source as 'sdk' in sync/async set/push methods --- src/pluggedinkit/__init__.py | 2 ++ src/pluggedinkit/services/clipboard.py | 4 ++++ src/pluggedinkit/types.py | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/src/pluggedinkit/__init__.py b/src/pluggedinkit/__init__.py index 7c88733..a9e4633 100644 --- a/src/pluggedinkit/__init__.py +++ b/src/pluggedinkit/__init__.py @@ -26,6 +26,7 @@ ClipboardDeleteResponse, ClipboardEncoding, ClipboardVisibility, + ClipboardSource, # Document types Document, DocumentWithContent, @@ -61,6 +62,7 @@ "ClipboardDeleteResponse", "ClipboardEncoding", "ClipboardVisibility", + "ClipboardSource", # Document types "Document", "DocumentWithContent", diff --git a/src/pluggedinkit/services/clipboard.py b/src/pluggedinkit/services/clipboard.py index bf8d1ed..9c4306b 100644 --- a/src/pluggedinkit/services/clipboard.py +++ b/src/pluggedinkit/services/clipboard.py @@ -78,6 +78,7 @@ def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": "sdk", # Hardcoded: SDK always uses 'sdk' source } if created_by_tool: @@ -111,6 +112,7 @@ def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": "sdk", # Hardcoded: SDK always uses 'sdk' source } if created_by_tool: @@ -236,6 +238,7 @@ async def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": "sdk", # Hardcoded: SDK always uses 'sdk' source } if created_by_tool: @@ -269,6 +272,7 @@ async def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, + "source": "sdk", # Hardcoded: SDK always uses 'sdk' source } if created_by_tool: diff --git a/src/pluggedinkit/types.py b/src/pluggedinkit/types.py index bff3765..78fa184 100644 --- a/src/pluggedinkit/types.py +++ b/src/pluggedinkit/types.py @@ -283,6 +283,13 @@ class ClipboardVisibility(str, Enum): PUBLIC = "public" +class ClipboardSource(str, Enum): + """Clipboard data source""" + UI = "ui" + SDK = "sdk" + MCP = "mcp" + + class ClipboardEntry(BaseModel): """Clipboard entry model""" uuid: str @@ -295,6 +302,7 @@ class ClipboardEntry(BaseModel): visibility: ClipboardVisibility = ClipboardVisibility.PRIVATE created_by_tool: Optional[str] = Field(None, alias="createdByTool") created_by_model: Optional[str] = Field(None, alias="createdByModel") + source: ClipboardSource = ClipboardSource.UI created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") expires_at: Optional[datetime] = Field(None, alias="expiresAt") From 783f12a944ec5a017a918292e7820d9d86e336e5 Mon Sep 17 00:00:00 2001 From: Cem Karaca Date: Sat, 29 Nov 2025 08:29:03 -0500 Subject: [PATCH 2/5] refactor: Make source field optional with DEFAULT_CLIPBOARD_SOURCE - Add DEFAULT_CLIPBOARD_SOURCE constant for backward compatibility - Make ClipboardEntry.source Optional to handle older API responses - Export constant from __init__.py Addresses code review feedback about breaking existing call sites. --- src/pluggedinkit/__init__.py | 2 ++ src/pluggedinkit/types.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pluggedinkit/__init__.py b/src/pluggedinkit/__init__.py index a9e4633..f4c0650 100644 --- a/src/pluggedinkit/__init__.py +++ b/src/pluggedinkit/__init__.py @@ -27,6 +27,7 @@ ClipboardEncoding, ClipboardVisibility, ClipboardSource, + DEFAULT_CLIPBOARD_SOURCE, # Document types Document, DocumentWithContent, @@ -63,6 +64,7 @@ "ClipboardEncoding", "ClipboardVisibility", "ClipboardSource", + "DEFAULT_CLIPBOARD_SOURCE", # Document types "Document", "DocumentWithContent", diff --git a/src/pluggedinkit/types.py b/src/pluggedinkit/types.py index 78fa184..f6bfed4 100644 --- a/src/pluggedinkit/types.py +++ b/src/pluggedinkit/types.py @@ -290,6 +290,10 @@ class ClipboardSource(str, Enum): MCP = "mcp" +# Default source for backward compatibility with older data +DEFAULT_CLIPBOARD_SOURCE = ClipboardSource.UI + + class ClipboardEntry(BaseModel): """Clipboard entry model""" uuid: str @@ -302,7 +306,8 @@ class ClipboardEntry(BaseModel): visibility: ClipboardVisibility = ClipboardVisibility.PRIVATE created_by_tool: Optional[str] = Field(None, alias="createdByTool") created_by_model: Optional[str] = Field(None, alias="createdByModel") - source: ClipboardSource = ClipboardSource.UI + # Optional for backward compatibility with older API responses + source: Optional[ClipboardSource] = DEFAULT_CLIPBOARD_SOURCE created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") expires_at: Optional[datetime] = Field(None, alias="expiresAt") From 17c7b5f7343b83274d23cb439c15cc895bb6645a Mon Sep 17 00:00:00 2001 From: Cem Karaca Date: Sat, 29 Nov 2025 08:40:10 -0500 Subject: [PATCH 3/5] docs: Add clipboard operations with source field documentation - Add Clipboard Operations section to README - Document set, get, push, pop, list, and delete operations - Add ClipboardEntry structure with source field - Note that source is auto-set to ClipboardSource.SDK when using this SDK - Add Clipboard/Memory to Features list --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/README.md b/README.md index 088858a..71ac9b7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ asyncio.run(main()) - 📄 **Document Management** - Full CRUD operations with type safety - 🔍 **Semantic Search** - AI-powered document search - 🤖 **RAG Integration** - Natural language queries to your knowledge base +- 📋 **Clipboard/Memory** - Persistent key-value storage for MCP tools and AI agents - 📤 **File Uploads** - Support for various file formats - 🔄 **Version Control** - Document versioning and history - ⚡ **Async Support** - Both sync and async clients @@ -377,6 +378,96 @@ async def rag_operations(): asyncio.run(rag_operations()) ``` +### Clipboard Operations + +The clipboard provides persistent key-value storage for MCP tools and AI agents. + +#### Set Named Entry + +```python +entry = client.clipboard.set( + name="user_preferences", + value='{"theme": "dark", "lang": "en"}', + content_type="application/json", + encoding="utf-8", + visibility="private", + ttl_seconds=86400 # 24 hours +) + +print(f"Created entry: {entry.uuid}") +print(f"Source: {entry.source}") # 'sdk' - automatically set +``` + +#### Get Entry + +```python +# By name +entry = client.clipboard.get(name="user_preferences") +print(entry.value) + +# By index (stack access) +latest = client.clipboard.get(idx=0) +``` + +#### Push to Stack + +```python +entry = client.clipboard.push( + value="Processing step 1 result", + content_type="text/plain" +) + +print(f"Pushed to index: {entry.idx}") +``` + +#### Pop from Stack + +```python +entry = client.clipboard.pop() +if entry: + print(f"Popped value: {entry.value}") +``` + +#### List and Delete + +```python +# List all entries +response = client.clipboard.list() +for entry in response.entries: + label = entry.name or f"idx:{entry.idx}" + print(f"{label} - source: {entry.source}") + +# Delete by name +client.clipboard.delete(name="old_entry") + +# Clear all +client.clipboard.clear_all() +``` + +#### Clipboard Entry Structure + +```python +from pluggedinkit import ClipboardEntry, ClipboardSource + +# ClipboardEntry fields: +# - uuid: str +# - name: Optional[str] # Semantic key +# - idx: Optional[int] # Stack index +# - value: str +# - content_type: str +# - encoding: "utf-8" | "base64" | "hex" +# - size_bytes: int +# - visibility: "private" | "workspace" | "public" +# - created_by_tool: Optional[str] +# - created_by_model: Optional[str] +# - source: Optional[ClipboardSource] # Auto-set: ui, sdk, or mcp +# - created_at: datetime +# - updated_at: datetime +# - expires_at: Optional[datetime] +``` + +> **Note**: The `source` field is automatically set to `ClipboardSource.SDK` when using this SDK. It indicates how the entry was created (UI, SDK, or MCP proxy). + ### Error Handling ```python From 029ad0489a0244a5c27e8ccb1429d5115a23ac5e Mon Sep 17 00:00:00 2001 From: Cem Karaca Date: Sat, 29 Nov 2025 09:38:00 -0500 Subject: [PATCH 4/5] fix: Improve ttl_seconds validation and clear_all counter logic - Use 'is not None' check for ttl_seconds instead of truthiness - Add explicit validation that ttl_seconds > 0 when provided - Fix clear_all to only increment counter when delete() returns True - Applied to both sync and async service classes (4 locations each) --- src/pluggedinkit/services/clipboard.py | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/pluggedinkit/services/clipboard.py b/src/pluggedinkit/services/clipboard.py index 9c4306b..10f9119 100644 --- a/src/pluggedinkit/services/clipboard.py +++ b/src/pluggedinkit/services/clipboard.py @@ -85,7 +85,9 @@ def set( payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + if ttl_seconds is not None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be greater than 0 when provided") payload["ttlSeconds"] = ttl_seconds response = self.client.request("POST", "/api/clipboard", json=payload) @@ -119,7 +121,9 @@ def push( payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + if ttl_seconds is not None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be greater than 0 when provided") payload["ttlSeconds"] = ttl_seconds response = self.client.request("POST", "/api/clipboard/push", json=payload) @@ -167,11 +171,13 @@ def clear_all(self) -> int: for entry in entries: try: + success = False if entry.name: - self.delete(name=entry.name) + success = self.delete(name=entry.name) elif entry.idx is not None: - self.delete(idx=entry.idx) - deleted += 1 + success = self.delete(idx=entry.idx) + if success: + deleted += 1 except PluggedInError: pass @@ -245,7 +251,9 @@ async def set( payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + if ttl_seconds is not None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be greater than 0 when provided") payload["ttlSeconds"] = ttl_seconds response = await self.client.request("POST", "/api/clipboard", json=payload) @@ -279,7 +287,9 @@ async def push( payload["createdByTool"] = created_by_tool if created_by_model: payload["createdByModel"] = created_by_model - if ttl_seconds: + if ttl_seconds is not None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be greater than 0 when provided") payload["ttlSeconds"] = ttl_seconds response = await self.client.request("POST", "/api/clipboard/push", json=payload) @@ -327,11 +337,13 @@ async def clear_all(self) -> int: for entry in entries: try: + success = False if entry.name: - await self.delete(name=entry.name) + success = await self.delete(name=entry.name) elif entry.idx is not None: - await self.delete(idx=entry.idx) - deleted += 1 + success = await self.delete(idx=entry.idx) + if success: + deleted += 1 except PluggedInError: pass From e996090e01203637b60447185aaff1a7d388dc35 Mon Sep 17 00:00:00 2001 From: Cem Karaca Date: Sat, 29 Nov 2025 10:02:34 -0500 Subject: [PATCH 5/5] refactor: Use ClipboardSource enum instead of string literal - Import ClipboardSource from types - Replace hardcoded "sdk" string with ClipboardSource.SDK.value - Consistent with enum usage across the SDK --- src/pluggedinkit/services/clipboard.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pluggedinkit/services/clipboard.py b/src/pluggedinkit/services/clipboard.py index 10f9119..610d3cd 100644 --- a/src/pluggedinkit/services/clipboard.py +++ b/src/pluggedinkit/services/clipboard.py @@ -12,6 +12,7 @@ ClipboardPushRequest, ClipboardResponse, ClipboardSetRequest, + ClipboardSource, ) if TYPE_CHECKING: @@ -78,7 +79,7 @@ def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, - "source": "sdk", # Hardcoded: SDK always uses 'sdk' source + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: @@ -114,7 +115,7 @@ def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, - "source": "sdk", # Hardcoded: SDK always uses 'sdk' source + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: @@ -244,7 +245,7 @@ async def set( "contentType": content_type, "encoding": encoding, "visibility": visibility, - "source": "sdk", # Hardcoded: SDK always uses 'sdk' source + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: @@ -280,7 +281,7 @@ async def push( "contentType": content_type, "encoding": encoding, "visibility": visibility, - "source": "sdk", # Hardcoded: SDK always uses 'sdk' source + "source": ClipboardSource.SDK.value, # SDK always uses 'sdk' source } if created_by_tool: