Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,13 @@ Positive:
<image>nextcloud/context_agent</image>
<image-tag>1.2.2</image-tag>
</docker-install>
<routes>
<route>
<url>mcp</url>
<verb>POST,GET,DELETE</verb>
<access_level>PUBLIC</access_level>
<headers_to_exclude>[]</headers_to_exclude>
</route>
</routes>
</external-app>
</info>
3 changes: 3 additions & 0 deletions ex_app/lib/all_tools/audio2text.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from langchain_core.tools import tool
from nc_py_api import Nextcloud

from ex_app.lib.all_tools.lib.context import get_nextcloud
from ex_app.lib.all_tools.lib.files import get_file_id_from_file_url
from ex_app.lib.all_tools.lib.task_processing import run_task
from ex_app.lib.all_tools.lib.decorator import safe_tool
Expand All @@ -18,6 +19,8 @@ def transcribe_file(file_url: str) -> str:
:param file_url: The file URL to the media file in nextcloud (The user can input this using the smart picker for example)
:return: the transcription result
"""
nonlocal nc
nc = get_nextcloud(nc)
task_input = {
'input': get_file_id_from_file_url(file_url),
}
Expand Down
11 changes: 11 additions & 0 deletions ex_app/lib/all_tools/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import xml.etree.ElementTree as ET
import vobject

from ex_app.lib.all_tools.lib.context import get_nextcloud
from ex_app.lib.all_tools.lib.decorator import safe_tool, dangerous_tool, timed_memoize
from ex_app.lib.all_tools.lib.freebusy_finder import find_available_slots, round_to_nearest_half_hour
from ex_app.lib.logger import log
Expand All @@ -27,6 +28,9 @@ def list_calendars():
List all existing calendars by name
:return:
"""
nonlocal nc
nc = get_nextcloud(nc)

principal = nc.cal.principal()
calendars = principal.calendars()
return ", ".join([cal.name for cal in calendars])
Expand All @@ -48,6 +52,8 @@ def schedule_event(calendar_name: str, title: str, description: str, start_date:
:param timezone: Timezone (e.g., 'America/New_York').
:return: bool
"""
nonlocal nc
nc = get_nextcloud(nc)

# Parse date and times
start_date = datetime.strptime(start_date, "%Y-%m-%d")
Expand Down Expand Up @@ -125,6 +131,9 @@ def find_free_time_slot_in_calendar(participants: list[str], slot_duration: Opti
:param end_time: the end time of the range within which to check for free slots (by default this will be 7 days after start_time; use the following format: 2025-01-31)
:return:
"""
nonlocal nc
nc = get_nextcloud(nc)

me = nc.ocs('GET', '/ocs/v2.php/cloud/user')

attendees = 'ORGANIZER:mailto:'+me['email']+'\n'
Expand Down Expand Up @@ -198,6 +207,8 @@ def add_task(calendar_name: str, title: str, description: str, due_date: Optiona
:param timezone: Timezone (e.g., 'America/New_York'). Is required if there is a specified due date.
:return: bool
"""
nonlocal nc
nc = get_nextcloud(nc)

# Create task
c = Calendar()
Expand Down
14 changes: 11 additions & 3 deletions ex_app/lib/all_tools/files.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
from fastmcp.server.dependencies import get_context
from langchain_core.tools import tool
from nc_py_api import Nextcloud

from typing import Optional

from ex_app.lib.all_tools.lib.context import get_nextcloud
from ex_app.lib.all_tools.lib.decorator import safe_tool, dangerous_tool


Expand All @@ -17,7 +19,9 @@ def get_file_content(file_path: str):
Get the content of a file
:param file_path: the path of the file
:return:
"""
"""
nonlocal nc
nc = get_nextcloud(nc)

user_id = nc.ocs('GET', '/ocs/v2.php/cloud/user')["id"]

Expand All @@ -34,7 +38,9 @@ def get_folder_tree(depth: int):
Get the folder tree of the user
:param depth: the depth of the returned folder tree
:return:
"""
"""
nonlocal nc
nc = get_nextcloud(nc)

return nc.ocs('GET', '/ocs/v2.php/apps/files/api/v1/folder-tree', json={'depth': depth}, response_type='json')

Expand All @@ -45,7 +51,9 @@ def create_public_sharing_link(path: str):
Creates a public sharing link for a file or folder
:param path: the path of the file or folder
:return:
"""
"""
nonlocal nc
nc = get_nextcloud(nc)

response = nc.ocs('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', json={
'path': path,
Expand Down
11 changes: 11 additions & 0 deletions ex_app/lib/all_tools/lib/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2024 LangChain, Inc.
# SPDX-License-Identifier: MIT
from fastmcp.server.dependencies import get_context


def get_nextcloud(nc):
try:
ctx = get_context()
return ctx.get_state('nextcloud')
except RuntimeError:
return nc
16 changes: 16 additions & 0 deletions ex_app/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@

from ex_app.lib.agent import react
from ex_app.lib.logger import log
from ex_app.lib.mcp_server import UserAuthMiddleware, ToolListMiddleware
from ex_app.lib.provider import provider
from ex_app.lib.tools import get_categories

from contextvars import ContextVar
from gettext import translation
from fastmcp import FastMCP

mcp = FastMCP(name="nextcloud")
mcp.add_middleware(UserAuthMiddleware())
mcp.add_middleware(ToolListMiddleware(mcp))
mcp.stateless_http = True
http_mcp_app = mcp.http_app("/", transport="http")

fast_app = FastAPI(lifespan=http_mcp_app.lifespan)

app_enabled = Event()

Expand All @@ -40,6 +49,11 @@ def _(text):

@asynccontextmanager
async def lifespan(app: FastAPI):
async with exapp_lifespan(app):
async with http_mcp_app.lifespan(app):
yield
@asynccontextmanager
async def exapp_lifespan(app: FastAPI):
set_handlers(app, enabled_handler)
start_bg_task()
nc = NextcloudApp()
Expand Down Expand Up @@ -176,6 +190,8 @@ def start_bg_task():
loop = asyncio.get_event_loop()
loop.create_task(background_thread_task())

APP.mount("/mcp", http_mcp_app)

if __name__ == "__main__":
# Wrapper around `uvicorn.run`.
# You are free to call it directly, with just using the `APP_HOST` and `APP_PORT` variables from the environment.
Expand Down
46 changes: 46 additions & 0 deletions ex_app/lib/mcp_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2024 LangChain, Inc.
# SPDX-License-Identifier: MIT
import time
from nc_py_api import NextcloudApp
from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext
from fastmcp.tools import Tool
from mcp import types as mt
from ex_app.lib.tools import get_tools

class UserAuthMiddleware(Middleware):
async def on_message(self, context: MiddlewareContext, call_next):
# Middleware stores user info in context state
user = context.fastmcp_context.request_context.request.headers.get("Authorization")
if user.startswith("Bearer "):
user = user[len("Bearer "):]
nc = NextcloudApp()
nc.set_user(user)
context.fastmcp_context.set_state("nextcloud", nc)
return await call_next(context)


LAST_MCP_TOOL_UPDATE = 0


class ToolListMiddleware(Middleware):
def __init__(self, mcp):
self.mcp = mcp

async def on_message(
self,
context: MiddlewareContext[mt.ListToolsRequest],
call_next: CallNext[mt.ListToolsRequest, list[Tool]],
) -> list[Tool]:
global LAST_MCP_TOOL_UPDATE
if LAST_MCP_TOOL_UPDATE + 60 < time.time():
safe, dangerous = await get_tools(context.fastmcp_context.get_state("nextcloud"))
tools = await self.mcp.get_tools()
if LAST_MCP_TOOL_UPDATE + 60 < time.time():
for tool in tools.keys():
self.mcp.remove_tool(tool)
for tool in safe + dangerous:
if not hasattr(tool, "func") or tool.func is None:
continue
self.mcp.tool()(tool.func)
Comment thread
lukasdotcom marked this conversation as resolved.
Outdated
LAST_MCP_TOOL_UPDATE = time.time()
return await call_next(context)
1 change: 0 additions & 1 deletion ex_app/lib/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import json
from os.path import dirname

from langchain_mcp_adapters.client import MultiServerMCPClient
from nc_py_api import Nextcloud
from ex_app.lib.all_tools.lib.decorator import timed_memoize

Expand Down
Loading
Loading