Skip to content

Commit 05ac05c

Browse files
committed
FEAT: add update delete tool
1 parent 3fdb4c4 commit 05ac05c

File tree

3 files changed

+426
-3
lines changed

3 files changed

+426
-3
lines changed

src/mcp_server_qdrant/mcp_server.py

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def format_entry(self, entry: Entry) -> str:
5959
Feel free to override this method in your subclass to customize the format of the entry.
6060
"""
6161
entry_metadata = json.dumps(entry.metadata) if entry.metadata else ""
62-
return f"<entry><content>{entry.content}</content><metadata>{entry_metadata}</metadata></entry>"
62+
entry_id = f"<id>{entry.id}</id>" if entry.id else ""
63+
return f"<entry>{entry_id}<content>{entry.content}</content><metadata>{entry_metadata}</metadata></entry>"
6364

6465
def setup_tools(self):
6566
"""
@@ -68,6 +69,10 @@ def setup_tools(self):
6869

6970
async def store(
7071
ctx: Context,
72+
id: Annotated[
73+
str | None,
74+
Field(description="Point ID. If omitted, a new point is created."),
75+
],
7176
information: Annotated[str, Field(description="Text to store")],
7277
collection_name: Annotated[
7378
str, Field(description="The collection to store the information in")
@@ -93,7 +98,7 @@ async def store(
9398
"""
9499
await ctx.debug(f"Storing information {information} in Qdrant")
95100

96-
entry = Entry(content=information, metadata=metadata)
101+
entry = Entry(content=information, metadata=metadata, id=id)
97102

98103
await self.qdrant_connector.store(entry, collection_name=collection_name)
99104
if collection_name:
@@ -160,16 +165,147 @@ async def find(
160165
store_foo, {"collection_name": self.qdrant_settings.collection_name}
161166
)
162167

168+
# Add new tools for point operations
169+
async def get_point(
170+
ctx: Context,
171+
point_id: Annotated[
172+
str, Field(description="The ID of the point to retrieve")
173+
],
174+
collection_name: Annotated[
175+
str, Field(description="The collection to get the point from")
176+
],
177+
) -> str:
178+
"""
179+
Get a specific point by its ID.
180+
:param ctx: The context for the request.
181+
:param point_id: The ID of the point to retrieve.
182+
:param collection_name: The name of the collection to get the point from.
183+
:return: The point information or error message.
184+
"""
185+
await ctx.debug(
186+
f"Getting point {point_id} from collection {collection_name}"
187+
)
188+
189+
entry = await self.qdrant_connector.get_point_by_id(
190+
point_id, collection_name=collection_name
191+
)
192+
193+
if entry:
194+
return self.format_entry(entry)
195+
else:
196+
return f"Point with ID {point_id} not found in collection {collection_name}"
197+
198+
async def delete_point(
199+
ctx: Context,
200+
point_id: Annotated[
201+
str, Field(description="The ID of the point to delete")
202+
],
203+
collection_name: Annotated[
204+
str, Field(description="The collection to delete the point from")
205+
],
206+
) -> str:
207+
"""
208+
Delete a specific point by its ID.
209+
:param ctx: The context for the request.
210+
:param point_id: The ID of the point to delete.
211+
:param collection_name: The name of the collection to delete the point from.
212+
:return: Success or error message.
213+
"""
214+
await ctx.debug(
215+
f"Deleting point {point_id} from collection {collection_name}"
216+
)
217+
218+
success = await self.qdrant_connector.delete_point_by_id(
219+
point_id, collection_name=collection_name
220+
)
221+
222+
if success:
223+
return f"Successfully deleted point {point_id} from collection {collection_name}"
224+
else:
225+
return f"Failed to delete point {point_id} - point not found or collection doesn't exist"
226+
227+
async def update_point_payload(
228+
ctx: Context,
229+
point_id: Annotated[
230+
str, Field(description="The ID of the point to update")
231+
],
232+
collection_name: Annotated[
233+
str, Field(description="The collection containing the point")
234+
],
235+
metadata: Annotated[
236+
Metadata,
237+
Field(
238+
description="New metadata to set for the point. Any json is accepted."
239+
),
240+
],
241+
) -> str:
242+
"""
243+
Update the payload (metadata) of a specific point by its ID.
244+
:param ctx: The context for the request.
245+
:param point_id: The ID of the point to update.
246+
:param collection_name: The name of the collection containing the point.
247+
:param metadata: New metadata to set for the point.
248+
:return: Success or error message.
249+
"""
250+
await ctx.debug(
251+
f"Updating payload for point {point_id} in collection {collection_name}"
252+
)
253+
254+
success = await self.qdrant_connector.update_point_payload(
255+
point_id, metadata, collection_name=collection_name
256+
)
257+
258+
if success:
259+
return f"Successfully updated payload for point {point_id} in collection {collection_name}"
260+
else:
261+
return f"Failed to update payload for point {point_id} - point not found or collection doesn't exist"
262+
263+
# Apply collection name defaults to new tools
264+
get_point_foo = get_point
265+
delete_point_foo = delete_point
266+
update_point_payload_foo = update_point_payload
267+
268+
if self.qdrant_settings.collection_name:
269+
get_point_foo = make_partial_function(
270+
get_point_foo, {"collection_name": self.qdrant_settings.collection_name}
271+
)
272+
delete_point_foo = make_partial_function(
273+
delete_point_foo,
274+
{"collection_name": self.qdrant_settings.collection_name},
275+
)
276+
update_point_payload_foo = make_partial_function(
277+
update_point_payload_foo,
278+
{"collection_name": self.qdrant_settings.collection_name},
279+
)
280+
163281
self.tool(
164282
find_foo,
165283
name="qdrant-find",
166284
description=self.tool_settings.tool_find_description,
167285
)
168286

287+
self.tool(
288+
get_point_foo,
289+
name="qdrant-get-point",
290+
description="Get a specific point by its ID from a Qdrant collection.",
291+
)
292+
169293
if not self.qdrant_settings.read_only:
170294
# Those methods can modify the database
171295
self.tool(
172296
store_foo,
173297
name="qdrant-store",
174298
description=self.tool_settings.tool_store_description,
175299
)
300+
301+
self.tool(
302+
delete_point_foo,
303+
name="qdrant-delete-point",
304+
description="Delete a specific point by its ID from a Qdrant collection.",
305+
)
306+
307+
self.tool(
308+
update_point_payload_foo,
309+
name="qdrant-update-point-payload",
310+
description="Update the payload (metadata) of a specific point by its ID.",
311+
)

src/mcp_server_qdrant/qdrant.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Entry(BaseModel):
1919
A single entry in the Qdrant collection.
2020
"""
2121

22+
id: str | None = None
2223
content: str
2324
metadata: Metadata | None = None
2425

@@ -76,14 +77,15 @@ async def store(self, entry: Entry, *, collection_name: str | None = None):
7677
# it should unlock usage of server-side inference.
7778
embeddings = await self._embedding_provider.embed_documents([entry.content])
7879

80+
point_id = uuid.uuid4().hex if entry.id is None else entry.id
7981
# Add to Qdrant
8082
vector_name = self._embedding_provider.get_vector_name()
8183
payload = {"document": entry.content, METADATA_PATH: entry.metadata}
8284
await self._client.upsert(
8385
collection_name=collection_name,
8486
points=[
8587
models.PointStruct(
86-
id=uuid.uuid4().hex,
88+
id=point_id,
8789
vector={vector_name: embeddings[0]},
8890
payload=payload,
8991
)
@@ -131,6 +133,7 @@ async def search(
131133

132134
return [
133135
Entry(
136+
id=str(result.id),
134137
content=result.payload["document"],
135138
metadata=result.payload.get("metadata"),
136139
)
@@ -168,3 +171,109 @@ async def _ensure_collection_exists(self, collection_name: str):
168171
field_name=field_name,
169172
field_schema=field_type,
170173
)
174+
175+
async def get_point_by_id(
176+
self,
177+
point_id: str,
178+
*,
179+
collection_name: str | None = None,
180+
) -> Entry | None:
181+
"""
182+
Retrieve a specific point by its ID.
183+
:param point_id: The ID of the point to retrieve.
184+
:param collection_name: The name of the collection, optional.
185+
:return: The entry if found, None otherwise.
186+
"""
187+
collection_name = collection_name or self._default_collection_name
188+
assert collection_name is not None
189+
190+
point = await self._client.retrieve(
191+
collection_name=collection_name,
192+
ids=[point_id],
193+
with_payload=True,
194+
with_vectors=False,
195+
)
196+
197+
if point and len(point) > 0:
198+
result = point[0]
199+
return Entry(
200+
id=str(result.id),
201+
content=result.payload["document"],
202+
metadata=result.payload.get("metadata"),
203+
)
204+
return None
205+
206+
async def delete_point_by_id(
207+
self,
208+
point_id: str,
209+
*,
210+
collection_name: str | None = None,
211+
) -> bool:
212+
"""
213+
Delete a specific point by its ID.
214+
:param point_id: The ID of the point to delete.
215+
:param collection_name: The name of the collection, optional.
216+
:return: True if the point was deleted, False if not found.
217+
"""
218+
collection_name = collection_name or self._default_collection_name
219+
assert collection_name is not None
220+
221+
collection_exists = await self._client.collection_exists(collection_name)
222+
if not collection_exists:
223+
return False
224+
225+
# Check if point exists before trying to delete
226+
existing_point = await self.get_point_by_id(
227+
point_id, collection_name=collection_name
228+
)
229+
if existing_point is None:
230+
return False
231+
232+
try:
233+
await self._client.delete(
234+
collection_name=collection_name,
235+
points_selector=models.PointIdsList(points=[point_id]),
236+
)
237+
return True
238+
except Exception as e:
239+
logger.error(f"Failed to delete point {point_id}: {e}")
240+
return False
241+
242+
async def update_point_payload(
243+
self,
244+
point_id: str,
245+
metadata: Metadata,
246+
*,
247+
collection_name: str | None = None,
248+
) -> bool:
249+
"""
250+
Update the payload (metadata) of a specific point by its ID.
251+
:param point_id: The ID of the point to update.
252+
:param metadata: New metadata to set for the point.
253+
:param collection_name: The name of the collection, optional.
254+
:return: True if the point was updated, False if not found.
255+
"""
256+
collection_name = collection_name or self._default_collection_name
257+
assert collection_name is not None
258+
259+
collection_exists = await self._client.collection_exists(collection_name)
260+
if not collection_exists:
261+
return False
262+
263+
# Check if point exists before trying to update
264+
existing_point = await self.get_point_by_id(
265+
point_id, collection_name=collection_name
266+
)
267+
if existing_point is None:
268+
return False
269+
270+
try:
271+
await self._client.set_payload(
272+
collection_name=collection_name,
273+
payload={METADATA_PATH: metadata},
274+
points=[point_id],
275+
)
276+
return True
277+
except Exception as e:
278+
logger.error(f"Failed to update payload for point {point_id}: {e}")
279+
return False

0 commit comments

Comments
 (0)