From 1b23493167e4891d98cd55596652c3f685f754cd Mon Sep 17 00:00:00 2001 From: oabrivard Date: Tue, 10 Mar 2026 07:33:49 +0100 Subject: [PATCH] feat(mcp): add MCP server with availability tool --- README.md | 14 ++++++++++++++ app/mcp/__init__.py | 3 +++ app/mcp/server.py | 19 +++++++++++++++++++ app/mcp/tools.py | 31 +++++++++++++++++++++++++++++++ app/mcp_main.py | 22 ++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 2 ++ 7 files changed, 92 insertions(+) create mode 100644 app/mcp/__init__.py create mode 100644 app/mcp/server.py create mode 100644 app/mcp/tools.py create mode 100644 app/mcp_main.py diff --git a/README.md b/README.md index f8bce85..af10f63 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,20 @@ curl -X POST "http://127.0.0.1:8000/a2a/rpc" \ }' ``` +### MCP server (availability tool) + +Run MCP on a dedicated port: + +```bash +uv run uvicorn app.mcp_main:app --host 0.0.0.0 --port 8001 +``` + +MCP streamable HTTP endpoint: + +```text +http://127.0.0.1:8001/mcp +``` + ### Manual unsubscribe digest ```bash diff --git a/app/mcp/__init__.py b/app/mcp/__init__.py new file mode 100644 index 0000000..f7feab5 --- /dev/null +++ b/app/mcp/__init__.py @@ -0,0 +1,3 @@ +from app.mcp.server import mcp + +__all__ = ["mcp"] diff --git a/app/mcp/server.py b/app/mcp/server.py new file mode 100644 index 0000000..1b44a73 --- /dev/null +++ b/app/mcp/server.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from mcp.server.fastmcp import FastMCP + +from app.mcp.tools import check_availability as check_availability_impl + +mcp = FastMCP( + "Personal Agent MCP", + streamable_http_path="/", +) + + +@mcp.tool(description="Check Google Calendar availability for a time range.") +def check_availability( + start: str, + end: str, + calendar_ids: list[str] | None = None, +) -> dict[str, object]: + return check_availability_impl(start=start, end=end, calendar_ids=calendar_ids) diff --git a/app/mcp/tools.py b/app/mcp/tools.py new file mode 100644 index 0000000..1594318 --- /dev/null +++ b/app/mcp/tools.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import logging +from typing import Any + +from app.config import get_settings +from app.core.service import CoreAgentService + +settings = get_settings() +core_service = CoreAgentService(settings=settings, logger=logging.getLogger("personal-agent.mcp")) + + +def check_availability( + start: str, end: str, calendar_ids: list[str] | None = None +) -> dict[str, Any]: + """Return free/busy availability for a time range on one or more calendars.""" + result = core_service.check_availability(start=start, end=end, calendar_ids=calendar_ids) + return { + "start": result.start, + "end": result.end, + "available": result.available, + "busy_slots": [ + { + "calendar_id": slot.calendar_id, + "start": slot.start, + "end": slot.end, + } + for slot in result.busy_slots + ], + "checked_calendars": result.checked_calendars, + } diff --git a/app/mcp_main.py b/app/mcp_main.py new file mode 100644 index 0000000..2ac826f --- /dev/null +++ b/app/mcp_main.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager + +from starlette.applications import Starlette +from starlette.routing import Mount + +from app.mcp import mcp + + +@asynccontextmanager +async def lifespan(_: Starlette): + async with mcp.session_manager.run(): + yield + + +app = Starlette( + routes=[ + Mount("/mcp", app=mcp.streamable_http_app()), + ], + lifespan=lifespan, +) diff --git a/pyproject.toml b/pyproject.toml index bf21227..12ab7d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.11" dependencies = [ "apscheduler", "fastapi", + "mcp", "google-api-python-client", "google-auth", "google-auth-oauthlib", diff --git a/uv.lock b/uv.lock index 4964b20..8ca2ec3 100644 --- a/uv.lock +++ b/uv.lock @@ -813,6 +813,7 @@ dependencies = [ { name = "google-api-python-client" }, { name = "google-auth" }, { name = "google-auth-oauthlib" }, + { name = "mcp" }, { name = "python-dotenv" }, { name = "strands-agents", extra = ["openai"] }, { name = "uvicorn", extra = ["standard"] }, @@ -825,6 +826,7 @@ requires-dist = [ { name = "google-api-python-client" }, { name = "google-auth" }, { name = "google-auth-oauthlib" }, + { name = "mcp" }, { name = "python-dotenv" }, { name = "strands-agents", extras = ["openai"] }, { name = "uvicorn", extras = ["standard"] },