Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4871fe9c5 | |||
| 26f679d86e | |||
| cf39a15db1 | |||
| 1f3c35f162 | |||
| 2bccc3dad9 | |||
| 959cb8b21a | |||
| f8a2410a0a | |||
| 03b984d5a7 | |||
| 57db18c6a3 | |||
| ea79e94842 | |||
| b0612cfa0f | |||
| 4e61d73da5 |
@@ -1,3 +1,28 @@
|
|||||||
|
## v0.46.2 (2025-11-22)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **smithery**: Enable JSON response format for scanner compatibility
|
||||||
|
|
||||||
|
## v0.46.1 (2025-11-22)
|
||||||
|
|
||||||
|
### Perf
|
||||||
|
|
||||||
|
- Optimize vector viz search performance
|
||||||
|
|
||||||
|
## v0.46.0 (2025-11-22)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- Add Smithery CLI deployment support
|
||||||
|
- Implement ADR-016 Smithery stateless deployment mode
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **smithery**: Add JSON Schema metadata to mcp-config endpoint
|
||||||
|
- **smithery**: Use container runtime pattern for config discovery
|
||||||
|
- Add Smithery lifespan and auth mode detection
|
||||||
|
|
||||||
## v0.45.0 (2025-11-22)
|
## v0.45.0 (2025-11-22)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:2e683fc3e18a248aa23b8022f2a3474b072b04fb851efe9b49f6b516a8944939
|
FROM docker.io/library/python:3.12-slim-trixie@sha256:b43ff04d5df04ad5cabb80890b7ef74e8410e3395b19af970dcd52d7a4bff921
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.9.11@sha256:5aa820129de0a600924f166aec9cb51613b15b68f1dcd2a02f31a500d2ede568 /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.9.11@sha256:5aa820129de0a600924f166aec9cb51613b15b68f1dcd2a02f31a500d2ede568 /uv /uvx /bin/
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -12,12 +12,12 @@
|
|||||||
# - Per-session app password authentication
|
# - Per-session app password authentication
|
||||||
# - Multi-user support via Smithery session config
|
# - Multi-user support via Smithery session config
|
||||||
|
|
||||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:2e683fc3e18a248aa23b8022f2a3474b072b04fb851efe9b49f6b516a8944939
|
FROM docker.io/library/python:3.12-slim-trixie@sha256:b43ff04d5df04ad5cabb80890b7ef74e8410e3395b19af970dcd52d7a4bff921
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install uv for fast dependency management
|
# Install uv for fast dependency management
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.9.10@sha256:29bd45092ea8902c0bbb7f0a338f0494a382b1f4b18355df5be270ade679ff1d /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.9.11@sha256:5aa820129de0a600924f166aec9cb51613b15b68f1dcd2a02f31a500d2ede568 /uv /uvx /bin/
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
# 1. git (required for caldav dependency from git)
|
# 1. git (required for caldav dependency from git)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
```markdown
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="astrolabe.svg" alt="Nextcloud MCP Server" width="128" height="128">
|
<img src="astrolabe.svg" alt="Nextcloud MCP Server" width="128" height="128">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Nextcloud MCP Server
|
# Nextcloud MCP Server
|
||||||
|
|
||||||
[](https://github.com/cbcoutinho/nextcloud-mcp-server/pkgs/container/nextcloud-mcp-server)
|
|
||||||
[](https://smithery.ai/server/@cbcoutinho/nextcloud-mcp-server)
|
[](https://smithery.ai/server/@cbcoutinho/nextcloud-mcp-server)
|
||||||
|
[](https://github.com/cbcoutinho/nextcloud-mcp-server/pkgs/container/nextcloud-mcp-server)
|
||||||
|
|
||||||
**A production-ready MCP server that connects AI assistants to your Nextcloud instance.**
|
**A production-ready MCP server that connects AI assistants to your Nextcloud instance.**
|
||||||
|
|
||||||
@@ -223,3 +224,4 @@ This project is licensed under the AGPL-3.0 License. See [LICENSE](./LICENSE) fo
|
|||||||
- [Model Context Protocol](https://github.com/modelcontextprotocol)
|
- [Model Context Protocol](https://github.com/modelcontextprotocol)
|
||||||
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
|
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
|
||||||
- [Nextcloud](https://nextcloud.com/)
|
- [Nextcloud](https://nextcloud.com/)
|
||||||
|
```
|
||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: nextcloud-mcp-server
|
name: nextcloud-mcp-server
|
||||||
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
||||||
type: application
|
type: application
|
||||||
version: 0.45.0
|
version: 0.46.2
|
||||||
appVersion: "0.45.0"
|
appVersion: "0.46.2"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
- mcp
|
- mcp
|
||||||
|
|||||||
@@ -1072,7 +1072,11 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
# ADR-016: Use Smithery lifespan for stateless mode, BasicAuth otherwise
|
# ADR-016: Use Smithery lifespan for stateless mode, BasicAuth otherwise
|
||||||
if deployment_mode == DeploymentMode.SMITHERY_STATELESS:
|
if deployment_mode == DeploymentMode.SMITHERY_STATELESS:
|
||||||
logger.info("Configuring MCP server for Smithery stateless mode")
|
logger.info("Configuring MCP server for Smithery stateless mode")
|
||||||
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_smithery)
|
# json_response=True returns plain JSON-RPC instead of SSE format,
|
||||||
|
# required for Smithery scanner compatibility
|
||||||
|
mcp = FastMCP(
|
||||||
|
"Nextcloud MCP", lifespan=app_lifespan_smithery, json_response=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Configuring MCP server for BasicAuth mode")
|
logger.info("Configuring MCP server for BasicAuth mode")
|
||||||
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_basic)
|
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_basic)
|
||||||
|
|||||||
@@ -218,71 +218,41 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch vectors for specific matching chunks from Qdrant
|
# Fetch vectors for specific matching chunks from Qdrant using batch retrieve
|
||||||
vector_fetch_start = time.perf_counter()
|
vector_fetch_start = time.perf_counter()
|
||||||
qdrant_client = await get_qdrant_client()
|
qdrant_client = await get_qdrant_client()
|
||||||
|
|
||||||
# Build filters for each specific chunk
|
|
||||||
from qdrant_client.models import FieldCondition, Filter, MatchValue
|
|
||||||
|
|
||||||
chunk_vectors_map = {} # Map (doc_id, chunk_start, chunk_end) -> vector
|
chunk_vectors_map = {} # Map (doc_id, chunk_start, chunk_end) -> vector
|
||||||
|
|
||||||
# Fetch vectors in batches by filtering on chunk-specific fields
|
# Collect point IDs from search results for batch retrieval
|
||||||
for result in search_results:
|
# point_id is the Qdrant internal ID returned by search algorithms
|
||||||
chunk_start = result.chunk_start_offset
|
point_ids = [r.point_id for r in search_results if r.point_id]
|
||||||
chunk_end = result.chunk_end_offset
|
|
||||||
|
|
||||||
# Build filter for this specific chunk
|
if point_ids:
|
||||||
must_conditions = [
|
# Single batch retrieve call instead of N sequential scroll calls
|
||||||
get_placeholder_filter(), # Always exclude placeholders from user-facing queries
|
# This is ~50x faster for 50 results (1 HTTP request vs 50)
|
||||||
FieldCondition(
|
points_response = await qdrant_client.retrieve(
|
||||||
key="doc_id",
|
|
||||||
match=MatchValue(value=result.id),
|
|
||||||
),
|
|
||||||
FieldCondition(
|
|
||||||
key="user_id",
|
|
||||||
match=MatchValue(value=username),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add chunk position filters if available
|
|
||||||
if chunk_start is not None:
|
|
||||||
must_conditions.append(
|
|
||||||
FieldCondition(
|
|
||||||
key="chunk_start_offset",
|
|
||||||
match=MatchValue(value=chunk_start),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if chunk_end is not None:
|
|
||||||
must_conditions.append(
|
|
||||||
FieldCondition(
|
|
||||||
key="chunk_end_offset",
|
|
||||||
match=MatchValue(value=chunk_end),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fetch this specific chunk vector
|
|
||||||
points_response = await qdrant_client.scroll(
|
|
||||||
collection_name=settings.get_collection_name(),
|
collection_name=settings.get_collection_name(),
|
||||||
scroll_filter=Filter(must=must_conditions),
|
ids=point_ids,
|
||||||
limit=1, # Only need the first match
|
|
||||||
with_vectors=["dense"],
|
with_vectors=["dense"],
|
||||||
with_payload=False,
|
with_payload=["doc_id", "chunk_start_offset", "chunk_end_offset"],
|
||||||
)
|
)
|
||||||
|
|
||||||
points = points_response[0]
|
# Build chunk_vectors_map from batch response
|
||||||
if points:
|
for point in points_response:
|
||||||
# Extract dense vector
|
|
||||||
point = points[0]
|
|
||||||
if point.vector is not None:
|
if point.vector is not None:
|
||||||
# If named vectors (dict), extract "dense"
|
# Extract dense vector (handle both named and unnamed vectors)
|
||||||
if isinstance(point.vector, dict):
|
if isinstance(point.vector, dict):
|
||||||
vector = point.vector.get("dense")
|
vector = point.vector.get("dense")
|
||||||
else:
|
else:
|
||||||
vector = point.vector
|
vector = point.vector
|
||||||
|
|
||||||
chunk_key = (result.id, chunk_start, chunk_end)
|
if vector is not None and point.payload:
|
||||||
chunk_vectors_map[chunk_key] = vector
|
doc_id = point.payload.get("doc_id")
|
||||||
|
chunk_start = point.payload.get("chunk_start_offset")
|
||||||
|
chunk_end = point.payload.get("chunk_end_offset")
|
||||||
|
chunk_key = (doc_id, chunk_start, chunk_end)
|
||||||
|
chunk_vectors_map[chunk_key] = vector
|
||||||
|
|
||||||
vector_fetch_duration = time.perf_counter() - vector_fetch_start
|
vector_fetch_duration = time.perf_counter() - vector_fetch_start
|
||||||
|
|
||||||
@@ -341,16 +311,23 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
|
|||||||
|
|
||||||
chunk_vectors = np.array(chunk_vectors)
|
chunk_vectors = np.array(chunk_vectors)
|
||||||
|
|
||||||
# Generate query embedding for visualization
|
# Reuse query embedding from search algorithm (avoids redundant embedding call)
|
||||||
query_embed_start = time.perf_counter()
|
query_embed_start = time.perf_counter()
|
||||||
from nextcloud_mcp_server.embedding.service import get_embedding_service
|
if search_algo.query_embedding is not None:
|
||||||
|
query_embedding = search_algo.query_embedding
|
||||||
|
logger.info(
|
||||||
|
f"Reusing query embedding from search algorithm "
|
||||||
|
f"(dimension={len(query_embedding)})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback: generate embedding if not available from search
|
||||||
|
from nextcloud_mcp_server.embedding.service import get_embedding_service
|
||||||
|
|
||||||
embedding_service = get_embedding_service()
|
embedding_service = get_embedding_service()
|
||||||
query_embedding = await embedding_service.embed(query)
|
query_embedding = await embedding_service.embed(query)
|
||||||
|
logger.info(f"Generated query embedding (dimension={len(query_embedding)})")
|
||||||
query_embed_duration = time.perf_counter() - query_embed_start
|
query_embed_duration = time.perf_counter() - query_embed_start
|
||||||
|
|
||||||
logger.info(f"Generated query embedding (dimension={len(query_embedding)})")
|
|
||||||
|
|
||||||
# Combine query vector with chunk vectors for PCA
|
# Combine query vector with chunk vectors for PCA
|
||||||
# Query will be the last point in the array
|
# Query will be the last point in the array
|
||||||
all_vectors = np.vstack([chunk_vectors, np.array([query_embedding])])
|
all_vectors = np.vstack([chunk_vectors, np.array([query_embedding])])
|
||||||
@@ -380,9 +357,19 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Apply PCA dimensionality reduction (768-dim → 3D) on normalized vectors
|
# Apply PCA dimensionality reduction (768-dim → 3D) on normalized vectors
|
||||||
|
# Run in thread pool to avoid blocking the event loop (CPU-bound)
|
||||||
pca_start = time.perf_counter()
|
pca_start = time.perf_counter()
|
||||||
pca = PCA(n_components=3)
|
|
||||||
coords_3d = pca.fit_transform(all_vectors_normalized)
|
def _compute_pca(vectors: np.ndarray) -> tuple[np.ndarray, PCA]:
|
||||||
|
pca = PCA(n_components=3)
|
||||||
|
coords = pca.fit_transform(vectors)
|
||||||
|
return coords, pca
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
coords_3d, pca = await anyio.to_thread.run_sync( # type: ignore[attr-defined]
|
||||||
|
lambda: _compute_pca(all_vectors_normalized)
|
||||||
|
)
|
||||||
pca_duration = time.perf_counter() - pca_start
|
pca_duration = time.perf_counter() - pca_start
|
||||||
|
|
||||||
# After fit, these attributes are guaranteed to be set
|
# After fit, these attributes are guaranteed to be set
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ class BM25SparseEmbeddingProvider:
|
|||||||
|
|
||||||
def encode(self, text: str) -> dict[str, Any]:
|
def encode(self, text: str) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate BM25 sparse embedding for a single text.
|
Generate BM25 sparse embedding for a single text (synchronous).
|
||||||
|
|
||||||
|
Note: For async contexts, prefer encode_async() to avoid blocking the event loop.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Input text to encode
|
text: Input text to encode
|
||||||
@@ -53,6 +55,23 @@ class BM25SparseEmbeddingProvider:
|
|||||||
"values": sparse_embedding.values.tolist(),
|
"values": sparse_embedding.values.tolist(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def encode_async(self, text: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate BM25 sparse embedding for a single text (async).
|
||||||
|
|
||||||
|
Runs CPU-bound BM25 encoding in thread pool to avoid blocking the event loop.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text to encode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with 'indices' and 'values' keys for Qdrant sparse vector
|
||||||
|
"""
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
# Run CPU-bound BM25 encoding in thread pool
|
||||||
|
return await anyio.to_thread.run_sync(lambda: self.encode(text)) # type: ignore[attr-defined]
|
||||||
|
|
||||||
async def encode_batch(self, texts: list[str]) -> list[dict[str, Any]]:
|
async def encode_batch(self, texts: list[str]) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Generate BM25 sparse embeddings for multiple texts (batched).
|
Generate BM25 sparse embeddings for multiple texts (batched).
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class SearchResult:
|
|||||||
page_number: Page number for PDF documents (None for other doc types)
|
page_number: Page number for PDF documents (None for other doc types)
|
||||||
chunk_index: Zero-based index of this chunk in the document
|
chunk_index: Zero-based index of this chunk in the document
|
||||||
total_chunks: Total number of chunks in the document
|
total_chunks: Total number of chunks in the document
|
||||||
|
point_id: Qdrant point ID for batch vector retrieval (None if not from Qdrant)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
@@ -153,6 +154,7 @@ class SearchResult:
|
|||||||
page_number: int | None = None
|
page_number: int | None = None
|
||||||
chunk_index: int = 0
|
chunk_index: int = 0
|
||||||
total_chunks: int = 1
|
total_chunks: int = 1
|
||||||
|
point_id: str | None = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Validate score is non-negative.
|
"""Validate score is non-negative.
|
||||||
@@ -172,8 +174,15 @@ class SearchAlgorithm(ABC):
|
|||||||
|
|
||||||
All search algorithms must implement the search() method with consistent
|
All search algorithms must implement the search() method with consistent
|
||||||
interface, allowing them to be used interchangeably.
|
interface, allowing them to be used interchangeably.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
query_embedding: The query embedding generated during the last search.
|
||||||
|
Available after search() completes for algorithms that use embeddings.
|
||||||
|
Can be reused by callers to avoid redundant embedding generation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
query_embedding: list[float] | None = None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def search(
|
async def search(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -101,11 +101,13 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
|||||||
# Generate dense embedding for semantic search
|
# Generate dense embedding for semantic search
|
||||||
embedding_service = get_embedding_service()
|
embedding_service = get_embedding_service()
|
||||||
dense_embedding = await embedding_service.embed(query)
|
dense_embedding = await embedding_service.embed(query)
|
||||||
|
# Store for reuse by callers (e.g., viz_routes PCA visualization)
|
||||||
|
self.query_embedding = dense_embedding
|
||||||
logger.debug(f"Generated dense embedding (dimension={len(dense_embedding)})")
|
logger.debug(f"Generated dense embedding (dimension={len(dense_embedding)})")
|
||||||
|
|
||||||
# Generate sparse embedding for BM25 keyword search
|
# Generate sparse embedding for BM25 keyword search
|
||||||
bm25_service = get_bm25_service()
|
bm25_service = get_bm25_service()
|
||||||
sparse_embedding = bm25_service.encode(query)
|
sparse_embedding = await bm25_service.encode_async(query)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Generated sparse embedding "
|
f"Generated sparse embedding "
|
||||||
f"({len(sparse_embedding['indices'])} non-zero terms)"
|
f"({len(sparse_embedding['indices'])} non-zero terms)"
|
||||||
@@ -218,6 +220,7 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
|||||||
page_number=result.payload.get("page_number"),
|
page_number=result.payload.get("page_number"),
|
||||||
chunk_index=result.payload.get("chunk_index", 0),
|
chunk_index=result.payload.get("chunk_index", 0),
|
||||||
total_chunks=result.payload.get("total_chunks", 1),
|
total_chunks=result.payload.get("total_chunks", 1),
|
||||||
|
point_id=str(result.id), # Qdrant point ID for batch retrieval
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
|||||||
# Generate embedding for query
|
# Generate embedding for query
|
||||||
embedding_service = get_embedding_service()
|
embedding_service = get_embedding_service()
|
||||||
query_embedding = await embedding_service.embed(query)
|
query_embedding = await embedding_service.embed(query)
|
||||||
|
# Store for reuse by callers (e.g., viz_routes PCA visualization)
|
||||||
|
self.query_embedding = query_embedding
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Generated embedding for query (dimension={len(query_embedding)})"
|
f"Generated embedding for query (dimension={len(query_embedding)})"
|
||||||
)
|
)
|
||||||
@@ -164,6 +166,7 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
|||||||
page_number=result.payload.get("page_number"),
|
page_number=result.payload.get("page_number"),
|
||||||
chunk_index=result.payload.get("chunk_index", 0),
|
chunk_index=result.payload.get("chunk_index", 0),
|
||||||
total_chunks=result.payload.get("total_chunks", 1),
|
total_chunks=result.payload.get("total_chunks", 1),
|
||||||
|
point_id=str(result.id), # Qdrant point ID for batch retrieval
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -335,27 +335,6 @@ def configure_semantic_tools(mcp: FastMCP):
|
|||||||
Note: Requires MCP client to support sampling. If sampling is unavailable,
|
Note: Requires MCP client to support sampling. If sampling is unavailable,
|
||||||
the tool gracefully degrades to returning documents with an explanation.
|
the tool gracefully degrades to returning documents with an explanation.
|
||||||
The client may prompt the user to approve the sampling request.
|
The client may prompt the user to approve the sampling request.
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> # Query about objectives across multiple apps
|
|
||||||
>>> result = await nc_semantic_search_answer(
|
|
||||||
... query="What are my Q1 2025 project goals?",
|
|
||||||
... ctx=ctx
|
|
||||||
... )
|
|
||||||
>>> print(result.generated_answer)
|
|
||||||
"Based on Document 1 (note: Project Kickoff), Document 2 (calendar event:
|
|
||||||
Q1 Planning Meeting), and Document 3 (deck card: Implement semantic search),
|
|
||||||
your main goals are: 1) Improve semantic search accuracy by 20%,
|
|
||||||
2) Deploy new embedding model, 3) Reduce indexing latency..."
|
|
||||||
|
|
||||||
>>> # Query about appointments
|
|
||||||
>>> result = await nc_semantic_search_answer(
|
|
||||||
... query="When is my next dentist appointment?",
|
|
||||||
... ctx=ctx,
|
|
||||||
... limit=10
|
|
||||||
... )
|
|
||||||
>>> len(result.sources) # Calendar events and related notes
|
|
||||||
3
|
|
||||||
"""
|
"""
|
||||||
# 1. Retrieve relevant documents via existing semantic search
|
# 1. Retrieve relevant documents via existing semantic search
|
||||||
search_response = await nc_semantic_search(
|
search_response = await nc_semantic_search(
|
||||||
|
|||||||
@@ -64,20 +64,6 @@ def configure_webdav_tools(mcp: FastMCP):
|
|||||||
- Text files are decoded to UTF-8
|
- Text files are decoded to UTF-8
|
||||||
- Documents (PDF, DOCX, etc.) are parsed and text is extracted
|
- Documents (PDF, DOCX, etc.) are parsed and text is extracted
|
||||||
- Other binary files are base64 encoded
|
- Other binary files are base64 encoded
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Read a text file
|
|
||||||
result = await nc_webdav_read_file("Documents/readme.txt")
|
|
||||||
logger.info(result['content']) # Decoded text content
|
|
||||||
|
|
||||||
# Read a PDF document (automatically parsed)
|
|
||||||
result = await nc_webdav_read_file("Documents/report.pdf")
|
|
||||||
logger.info(result['content']) # Extracted text from PDF
|
|
||||||
logger.info(result['parsing_metadata']) # Document parsing info
|
|
||||||
|
|
||||||
# Read a binary file
|
|
||||||
result = await nc_webdav_read_file("Images/photo.jpg")
|
|
||||||
logger.info(result['encoding']) # 'base64'
|
|
||||||
"""
|
"""
|
||||||
client = await get_client(ctx)
|
client = await get_client(ctx)
|
||||||
content, content_type = await client.webdav.read_file(path)
|
content, content_type = await client.webdav.read_file(path)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.45.0"
|
version = "0.46.2"
|
||||||
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
||||||
|
|||||||
@@ -1936,7 +1936,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.45.0"
|
version = "0.46.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
|
|||||||
Reference in New Issue
Block a user