Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b4318bde5 | |||
| 27fe066b23 | |||
| e94b8ff714 | |||
| e3a6894904 | |||
| 92b97bda00 | |||
| d5c6039296 |
@@ -89,6 +89,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv run pytest tests/integration/test_rag.py -v --log-cli-level=INFO --provider openai
|
uv run pytest tests/integration/test_rag.py -v --log-cli-level=INFO --provider openai
|
||||||
|
|
||||||
|
- name: Capture MCP container logs
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "=== MCP Container Logs ==="
|
||||||
|
docker compose logs mcp --tail=500
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
## v0.48.4 (2025-11-23)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- Add rate limit retry logic to OpenAI provider
|
||||||
|
|
||||||
## v0.48.3 (2025-11-23)
|
## v0.48.3 (2025-11-23)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -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.48.3
|
version: 0.48.4
|
||||||
appVersion: "0.48.3"
|
appVersion: "0.48.4"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
- mcp
|
- mcp
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: docker.io/library/nextcloud:32.0.2@sha256:ac08482d73ffd85d94069ba291bbd5fb39a70ff21502030a2e3e2d89a7246a48
|
image: docker.io/library/nextcloud:32.0.2@sha256:8cb1dc8c26944115469dd22f4965d2ed35bab9cf8c48d2bb052c8e9f83821ded
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 0.0.0.0:8080:80
|
- 0.0.0.0:8080:80
|
||||||
|
|||||||
@@ -7,13 +7,48 @@ Supports:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from openai import AsyncOpenAI
|
import anyio
|
||||||
|
from openai import AsyncOpenAI, RateLimitError
|
||||||
|
|
||||||
from .base import Provider
|
from .base import Provider
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Rate limit retry configuration
|
||||||
|
MAX_RETRIES = 5
|
||||||
|
INITIAL_RETRY_DELAY = 2.0 # seconds
|
||||||
|
MAX_RETRY_DELAY = 60.0 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
def retry_on_rate_limit(func):
|
||||||
|
"""Decorator to retry on OpenAI rate limit errors with exponential backoff."""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
retry_delay = INITIAL_RETRY_DELAY
|
||||||
|
last_error: Exception | None = None
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except RateLimitError as e:
|
||||||
|
last_error = e
|
||||||
|
if attempt < MAX_RETRIES:
|
||||||
|
logger.warning(
|
||||||
|
f"Rate limit hit (attempt {attempt}/{MAX_RETRIES}), "
|
||||||
|
f"retrying in {retry_delay:.1f}s..."
|
||||||
|
)
|
||||||
|
await anyio.sleep(retry_delay)
|
||||||
|
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
||||||
|
|
||||||
|
logger.error(f"Rate limit exceeded after {MAX_RETRIES} attempts")
|
||||||
|
raise last_error # type: ignore[misc]
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# Well-known embedding dimensions for OpenAI models
|
# Well-known embedding dimensions for OpenAI models
|
||||||
OPENAI_EMBEDDING_DIMENSIONS: dict[str, int] = {
|
OPENAI_EMBEDDING_DIMENSIONS: dict[str, int] = {
|
||||||
"text-embedding-3-small": 1536,
|
"text-embedding-3-small": 1536,
|
||||||
@@ -86,6 +121,7 @@ class OpenAIProvider(Provider):
|
|||||||
"""Whether this provider supports text generation."""
|
"""Whether this provider supports text generation."""
|
||||||
return self.generation_model is not None
|
return self.generation_model is not None
|
||||||
|
|
||||||
|
@retry_on_rate_limit
|
||||||
async def embed(self, text: str) -> list[float]:
|
async def embed(self, text: str) -> list[float]:
|
||||||
"""
|
"""
|
||||||
Generate embedding vector for text.
|
Generate embedding vector for text.
|
||||||
@@ -151,14 +187,8 @@ class OpenAIProvider(Provider):
|
|||||||
for i in range(0, len(texts), batch_size):
|
for i in range(0, len(texts), batch_size):
|
||||||
batch = texts[i : i + batch_size]
|
batch = texts[i : i + batch_size]
|
||||||
|
|
||||||
response = await self.client.embeddings.create(
|
# Use helper method with retry logic for each batch
|
||||||
input=batch,
|
batch_embeddings = await self._embed_batch_request(batch)
|
||||||
model=self.embedding_model,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort by index to maintain order
|
|
||||||
sorted_data = sorted(response.data, key=lambda x: x.index)
|
|
||||||
batch_embeddings = [item.embedding for item in sorted_data]
|
|
||||||
all_embeddings.extend(batch_embeddings)
|
all_embeddings.extend(batch_embeddings)
|
||||||
|
|
||||||
# Update dimension if not set
|
# Update dimension if not set
|
||||||
@@ -171,6 +201,17 @@ class OpenAIProvider(Provider):
|
|||||||
|
|
||||||
return all_embeddings
|
return all_embeddings
|
||||||
|
|
||||||
|
@retry_on_rate_limit
|
||||||
|
async def _embed_batch_request(self, batch: list[str]) -> list[list[float]]:
|
||||||
|
"""Make a single batch embedding request with retry logic."""
|
||||||
|
response = await self.client.embeddings.create(
|
||||||
|
input=batch,
|
||||||
|
model=self.embedding_model,
|
||||||
|
)
|
||||||
|
# Sort by index to maintain order
|
||||||
|
sorted_data = sorted(response.data, key=lambda x: x.index)
|
||||||
|
return [item.embedding for item in sorted_data]
|
||||||
|
|
||||||
def get_dimension(self) -> int:
|
def get_dimension(self) -> int:
|
||||||
"""
|
"""
|
||||||
Get embedding dimension.
|
Get embedding dimension.
|
||||||
@@ -194,6 +235,7 @@ class OpenAIProvider(Provider):
|
|||||||
)
|
)
|
||||||
return self._dimension
|
return self._dimension
|
||||||
|
|
||||||
|
@retry_on_rate_limit
|
||||||
async def generate(self, prompt: str, max_tokens: int = 500) -> str:
|
async def generate(self, prompt: str, max_tokens: int = 500) -> str:
|
||||||
"""
|
"""
|
||||||
Generate text from a prompt.
|
Generate text from a prompt.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.48.3"
|
version = "0.48.4"
|
||||||
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.48.3"
|
version = "0.48.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
|
|||||||
Reference in New Issue
Block a user