decodingtrust-agent-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent/__init__.py +30 -0
- agent/claudesdk/__init__.py +8 -0
- agent/claudesdk/example.py +221 -0
- agent/claudesdk/src/__init__.py +8 -0
- agent/claudesdk/src/agent.py +400 -0
- agent/claudesdk/src/mcp_proxy.py +409 -0
- agent/claudesdk/src/utils.py +420 -0
- agent/googleadk/__init__.py +15 -0
- agent/googleadk/example.py +237 -0
- agent/googleadk/src/__init__.py +12 -0
- agent/googleadk/src/agent.py +401 -0
- agent/googleadk/src/mcp_wrapper.py +163 -0
- agent/googleadk/src/utils.py +602 -0
- agent/langchain/__init__.py +8 -0
- agent/langchain/example.py +213 -0
- agent/langchain/src/__init__.py +8 -0
- agent/langchain/src/agent.py +645 -0
- agent/langchain/src/utils.py +433 -0
- agent/openaisdk/__init__.py +17 -0
- agent/openaisdk/example.py +228 -0
- agent/openaisdk/src/__init__.py +12 -0
- agent/openaisdk/src/agent.py +491 -0
- agent/openaisdk/src/agent_wrapper.py +143 -0
- agent/openaisdk/src/mcp_wrapper.py +395 -0
- agent/openaisdk/src/utils.py +493 -0
- agent/openclaw/__init__.py +10 -0
- agent/openclaw/example.py +251 -0
- agent/openclaw/src/__init__.py +14 -0
- agent/openclaw/src/agent.py +930 -0
- agent/openclaw/src/helpers/__init__.py +1 -0
- agent/openclaw/src/helpers/auth_helpers.py +55 -0
- agent/openclaw/src/mcp_proxy.py +564 -0
- agent/openclaw/src/plugin_generator.py +231 -0
- agent/openclaw/src/utils.py +341 -0
- agent/pocketflow/__init__.py +18 -0
- agent/pocketflow/example.py +221 -0
- agent/pocketflow/prompts/react_agent.py +46 -0
- agent/pocketflow/src/__init__.py +6 -0
- agent/pocketflow/src/agent.py +507 -0
- agent/pocketflow/src/agent_wrapper.py +159 -0
- agent/pocketflow/src/async_helper.py +92 -0
- agent/pocketflow/src/mcp_react_agent.py +279 -0
- agent/pocketflow/src/native_agent.py +74 -0
- agent/pocketflow/src/nodes.py +467 -0
- benchmark/__init__.py +0 -0
- benchmark/browser/benign.jsonl +34 -0
- benchmark/browser/direct.jsonl +85 -0
- benchmark/browser/indirect.jsonl +82 -0
- benchmark/code/benign.jsonl +0 -0
- benchmark/code/direct.jsonl +121 -0
- benchmark/code/indirect.jsonl +165 -0
- benchmark/crm/benign.jsonl +165 -0
- benchmark/crm/direct.jsonl +90 -0
- benchmark/crm/indirect.jsonl +150 -0
- benchmark/customer-service/benign.jsonl +160 -0
- benchmark/customer-service/direct.jsonl +100 -0
- benchmark/customer-service/indirect.jsonl +101 -0
- benchmark/finance/benign.jsonl +0 -0
- benchmark/finance/direct.jsonl +200 -0
- benchmark/finance/indirect.jsonl +200 -0
- benchmark/legal/benign.jsonl +0 -0
- benchmark/legal/direct.jsonl +200 -0
- benchmark/legal/indirect.jsonl +200 -0
- benchmark/macos/benign.jsonl +30 -0
- benchmark/macos/direct.jsonl +50 -0
- benchmark/macos/indirect.jsonl +50 -0
- benchmark/medical/benign.jsonl +642 -0
- benchmark/medical/direct.jsonl +229 -0
- benchmark/medical/indirect.jsonl +222 -0
- benchmark/os-filesystem/benign.jsonl +200 -0
- benchmark/os-filesystem/direct.jsonl +200 -0
- benchmark/os-filesystem/indirect.jsonl +200 -0
- benchmark/research/benign.jsonl +0 -0
- benchmark/research/direct.jsonl +119 -0
- benchmark/research/indirect.jsonl +125 -0
- benchmark/telecom/benign.jsonl +120 -0
- benchmark/telecom/direct.jsonl +161 -0
- benchmark/telecom/indirect.jsonl +166 -0
- benchmark/travel/benign.jsonl +130 -0
- benchmark/travel/direct.jsonl +105 -0
- benchmark/travel/indirect.jsonl +120 -0
- benchmark/windows/benign.jsonl +100 -0
- benchmark/windows/direct.jsonl +140 -0
- benchmark/windows/indirect.jsonl +107 -0
- benchmark/workflow/benign.jsonl +335 -0
- benchmark/workflow/direct.jsonl +78 -0
- benchmark/workflow/indirect.jsonl +107 -0
- cli/__init__.py +5 -0
- cli/main.py +182 -0
- cli/scaffold.py +334 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
- dt_arena/config/env.yaml +515 -0
- dt_arena/config/injection_mcp.yaml +430 -0
- dt_arena/config/mcp.yaml +642 -0
- dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
- dt_arena/envs/arxiv/docker-compose.yml +36 -0
- dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
- dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
- dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
- dt_arena/envs/atlassian/docker-compose.yml +72 -0
- dt_arena/envs/bigquery/docker-compose.yml +20 -0
- dt_arena/envs/booking/docker-compose.yml +59 -0
- dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
- dt_arena/envs/calendar/docker-compose.yml +42 -0
- dt_arena/envs/custom-website/docker-compose.yml +6 -0
- dt_arena/envs/customer_service/docker-compose.yml +59 -0
- dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
- dt_arena/envs/databricks/docker-compose.yml +51 -0
- dt_arena/envs/ecommerce/docker-compose.yml +6 -0
- dt_arena/envs/ers/docker-compose.yml +36 -0
- dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
- dt_arena/envs/finance/docker-compose.yml +23 -0
- dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
- dt_arena/envs/github/docker/docker-compose.yml +50 -0
- dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
- dt_arena/envs/gmail/docker-compose.yml +65 -0
- dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
- dt_arena/envs/google-form/docker-compose.yml +41 -0
- dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
- dt_arena/envs/googledocs/docker-compose.yml +78 -0
- dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
- dt_arena/envs/hospital/docker-compose.yml +27 -0
- dt_arena/envs/legal/docker-compose.yml +22 -0
- dt_arena/envs/linkedin/docker-compose.yml +63 -0
- dt_arena/envs/macos/docker-compose.yml +79 -0
- dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
- dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
- dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
- dt_arena/envs/paypal/docker-compose.yml +63 -0
- dt_arena/envs/research/docker-compose-hub.yml +13 -0
- dt_arena/envs/research/docker-compose.yml +24 -0
- dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
- dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
- dt_arena/envs/slack/docker-compose-hub.yml +28 -0
- dt_arena/envs/slack/docker-compose.yml +41 -0
- dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
- dt_arena/envs/snowflake/docker-compose.yml +44 -0
- dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
- dt_arena/envs/telecom/docker-compose.yml +17 -0
- dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
- dt_arena/envs/telegram/docker-compose.yml +62 -0
- dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
- dt_arena/envs/terminal/docker-compose.yml +26 -0
- dt_arena/envs/travel/docker-compose-hub.yml +19 -0
- dt_arena/envs/travel/docker-compose.yml +19 -0
- dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
- dt_arena/envs/whatsapp/docker-compose.yml +78 -0
- dt_arena/envs/windows/docker-compose.yml +71 -0
- dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
- dt_arena/envs/zoom/docker-compose.yml +40 -0
- dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
- dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
- dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
- dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
- dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
- dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
- dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
- dt_arena/injection_mcp_server/github/env_injection.py +206 -0
- dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
- dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
- dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
- dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
- dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
- dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
- dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
- dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
- dt_arena/injection_mcp_server/research/env_injection.py +616 -0
- dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
- dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
- dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
- dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
- dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
- dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
- dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
- dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
- dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
- dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
- dt_arena/mcp_server/atlassian/main.py +1554 -0
- dt_arena/mcp_server/atlassian/test_server.py +66 -0
- dt_arena/mcp_server/bigquery/main.py +333 -0
- dt_arena/mcp_server/booking/main.py +310 -0
- dt_arena/mcp_server/browser/main.py +1741 -0
- dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
- dt_arena/mcp_server/calendar/main.py +792 -0
- dt_arena/mcp_server/calendar/test_mcp.py +135 -0
- dt_arena/mcp_server/customer_service/main.py +1063 -0
- dt_arena/mcp_server/databricks/main.py +566 -0
- dt_arena/mcp_server/databricks/probe.py +102 -0
- dt_arena/mcp_server/ers/main.py +845 -0
- dt_arena/mcp_server/finance/__init__.py +87 -0
- dt_arena/mcp_server/finance/core/__init__.py +12 -0
- dt_arena/mcp_server/finance/core/data_loader.py +558 -0
- dt_arena/mcp_server/finance/core/portfolio.py +565 -0
- dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
- dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
- dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
- dt_arena/mcp_server/finance/injection/__init__.py +66 -0
- dt_arena/mcp_server/finance/injection/config.py +176 -0
- dt_arena/mcp_server/finance/injection/content.py +755 -0
- dt_arena/mcp_server/finance/injection/html.py +409 -0
- dt_arena/mcp_server/finance/injection/locations.py +167 -0
- dt_arena/mcp_server/finance/injection/methods.py +193 -0
- dt_arena/mcp_server/finance/injection/presets.py +1023 -0
- dt_arena/mcp_server/finance/main.py +361 -0
- dt_arena/mcp_server/finance/run_mcp.py +21 -0
- dt_arena/mcp_server/finance/run_web.py +26 -0
- dt_arena/mcp_server/finance/server/__init__.py +41 -0
- dt_arena/mcp_server/finance/server/extractor.py +1453 -0
- dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
- dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
- dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
- dt_arena/mcp_server/finance/server/mcp.py +451 -0
- dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
- dt_arena/mcp_server/finance/server/tools/account.py +88 -0
- dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
- dt_arena/mcp_server/finance/server/tools/social.py +73 -0
- dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
- dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
- dt_arena/mcp_server/finance/server/web.py +2139 -0
- dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
- dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
- dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
- dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
- dt_arena/mcp_server/github/main.py +441 -0
- dt_arena/mcp_server/gmail/main.py +1004 -0
- dt_arena/mcp_server/google_form/main.py +141 -0
- dt_arena/mcp_server/googledocs/main.py +458 -0
- dt_arena/mcp_server/hospital/mcp_server.py +458 -0
- dt_arena/mcp_server/legal/__init__.py +9 -0
- dt_arena/mcp_server/legal/core/__init__.py +14 -0
- dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
- dt_arena/mcp_server/legal/core/data_loader.py +266 -0
- dt_arena/mcp_server/legal/core/document_store.py +197 -0
- dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
- dt_arena/mcp_server/legal/main.py +89 -0
- dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
- dt_arena/mcp_server/legal/server/__init__.py +14 -0
- dt_arena/mcp_server/legal/server/mcp.py +2330 -0
- dt_arena/mcp_server/macos/client_test.py +270 -0
- dt_arena/mcp_server/macos/mcp_server.py +285 -0
- dt_arena/mcp_server/os-filesystem/main.py +1380 -0
- dt_arena/mcp_server/paypal/main.py +501 -0
- dt_arena/mcp_server/research/main.py +777 -0
- dt_arena/mcp_server/salesforce/main.py +2006 -0
- dt_arena/mcp_server/slack/main.py +318 -0
- dt_arena/mcp_server/snowflake/main.py +612 -0
- dt_arena/mcp_server/snowflake/probe.py +183 -0
- dt_arena/mcp_server/telecom/mcp_client.py +423 -0
- dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
- dt_arena/mcp_server/telegram/main.py +338 -0
- dt_arena/mcp_server/terminal/main.py +163 -0
- dt_arena/mcp_server/travel/client_test.py +16 -0
- dt_arena/mcp_server/travel/mcp_server.py +404 -0
- dt_arena/mcp_server/whatsapp/main.py +318 -0
- dt_arena/mcp_server/windows/client_test.py +270 -0
- dt_arena/mcp_server/windows/mcp_server.py +218 -0
- dt_arena/mcp_server/zoom/main.py +466 -0
- dt_arena/src/__init__.py +0 -0
- dt_arena/src/hooks/__init__.py +0 -0
- dt_arena/src/hooks/audit_log.py +30 -0
- dt_arena/src/hooks/hooks.json +3 -0
- dt_arena/src/run_benign.py +142 -0
- dt_arena/src/types/__init__.py +0 -0
- dt_arena/src/types/agent.py +441 -0
- dt_arena/src/types/attacks.py +2 -0
- dt_arena/src/types/environment.py +2 -0
- dt_arena/src/types/hooks.py +174 -0
- dt_arena/src/types/judge.py +52 -0
- dt_arena/src/types/red_teaming_trajectory.py +385 -0
- dt_arena/src/types/task.py +260 -0
- dt_arena/src/types/trajectory.py +315 -0
- dt_arena/utils/__init__.py +1 -0
- dt_arena/utils/atlassian/__init__.py +27 -0
- dt_arena/utils/atlassian/helpers.py +520 -0
- dt_arena/utils/bigquery/__init__.py +1 -0
- dt_arena/utils/bigquery/helpers.py +246 -0
- dt_arena/utils/calendar/__init__.py +1 -0
- dt_arena/utils/calendar/helpers.py +87 -0
- dt_arena/utils/customer_service/__init__.py +17 -0
- dt_arena/utils/customer_service/cs_env_client.py +940 -0
- dt_arena/utils/customer_service/helpers.py +339 -0
- dt_arena/utils/customer_service/judges/__init__.py +20 -0
- dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
- dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
- dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
- dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
- dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
- dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
- dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
- dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
- dt_arena/utils/customer_service/judges/text_utils.py +21 -0
- dt_arena/utils/databricks/__init__.py +2 -0
- dt_arena/utils/databricks/helpers.py +210 -0
- dt_arena/utils/finance/__init__.py +0 -0
- dt_arena/utils/finance/helpers.py +263 -0
- dt_arena/utils/github/__init__.py +1 -0
- dt_arena/utils/github/helpers.py +249 -0
- dt_arena/utils/gmail/__init__.py +1 -0
- dt_arena/utils/gmail/helpers.py +344 -0
- dt_arena/utils/google_form/__init__.py +2 -0
- dt_arena/utils/google_form/helpers.py +133 -0
- dt_arena/utils/legal/__init__.py +0 -0
- dt_arena/utils/legal/helpers.py +228 -0
- dt_arena/utils/macos/__init__.py +0 -0
- dt_arena/utils/macos/env_setup.py +215 -0
- dt_arena/utils/macos/helpers.py +61 -0
- dt_arena/utils/os_filesystem/__init__.py +1 -0
- dt_arena/utils/os_filesystem/helpers.py +366 -0
- dt_arena/utils/paypal/__init__.py +1 -0
- dt_arena/utils/paypal/helpers.py +178 -0
- dt_arena/utils/port_allocator.py +266 -0
- dt_arena/utils/research/__init__.py +0 -0
- dt_arena/utils/research/helpers.py +251 -0
- dt_arena/utils/salesforce/__init__.py +1 -0
- dt_arena/utils/salesforce/helpers.py +719 -0
- dt_arena/utils/slack/__init__.py +1 -0
- dt_arena/utils/slack/helpers.py +176 -0
- dt_arena/utils/snowflake/__init__.py +1 -0
- dt_arena/utils/snowflake/helpers.py +166 -0
- dt_arena/utils/telecom/__init__.py +1 -0
- dt_arena/utils/telecom/helpers.py +760 -0
- dt_arena/utils/telegram/__init__.py +0 -0
- dt_arena/utils/telegram/helpers.py +174 -0
- dt_arena/utils/terminal/__init__.py +0 -0
- dt_arena/utils/terminal/helpers.py +20 -0
- dt_arena/utils/travel/__init__.py +0 -0
- dt_arena/utils/travel/env_client.py +537 -0
- dt_arena/utils/travel/llm_judge.py +137 -0
- dt_arena/utils/travel/prompts.py +64 -0
- dt_arena/utils/utils/__init__.py +122 -0
- dt_arena/utils/whatsapp/__init__.py +0 -0
- dt_arena/utils/whatsapp/helpers.py +226 -0
- dt_arena/utils/windows/__init__.py +0 -0
- dt_arena/utils/windows/env_reset.py +224 -0
- dt_arena/utils/windows/env_setup.py +280 -0
- dt_arena/utils/windows/exfil_helpers.py +170 -0
- dt_arena/utils/windows/helpers.py +74 -0
- dt_arena/utils/zoom/__init__.py +1 -0
- dt_arena/utils/zoom/helpers.py +70 -0
- eval/__init__.py +1 -0
- eval/evaluation.py +426 -0
- eval/task_runner.py +449 -0
- utils/__init__.py +148 -0
- utils/agent_helpers.py +308 -0
- utils/agent_wrapper.py +189 -0
- utils/compose_utils.py +135 -0
- utils/config.py +77 -0
- utils/env_helpers.py +104 -0
- utils/eval_stats.py +88 -0
- utils/injection_helpers.py +429 -0
- utils/injection_mcp_helpers.py +152 -0
- utils/judge_helpers.py +181 -0
- utils/judge_utils.py +472 -0
- utils/llm.py +196 -0
- utils/logging.py +45 -0
- utils/mcp_helpers.py +232 -0
- utils/mcp_manager.py +235 -0
- utils/memory_guard.py +18 -0
- utils/red_teaming_sandbox.py +476 -0
- utils/reset_helpers.py +318 -0
- utils/resource_manager.py +370 -0
- utils/skill_helpers.py +447 -0
- utils/task_executor.py +904 -0
- utils/task_helpers.py +270 -0
- utils/template_helpers.py +179 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Calendar MCP Server for Google Calendar Sandbox.
|
|
4
|
+
Provides calendar and event management tools for AI assistants.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import httpx
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
try:
|
|
17
|
+
import yaml # type: ignore
|
|
18
|
+
except Exception:
|
|
19
|
+
yaml = None
|
|
20
|
+
|
|
21
|
+
# Calendar API config
|
|
22
|
+
CALENDAR_API_URL = os.getenv("CALENDAR_API_URL", "http://localhost:8032")
|
|
23
|
+
CALENDAR_API_BASE = f"{CALENDAR_API_URL}/calendar/v3"
|
|
24
|
+
|
|
25
|
+
# User authentication
|
|
26
|
+
USER_ACCESS_TOKEN = os.getenv("USER_ACCESS_TOKEN", "")
|
|
27
|
+
|
|
28
|
+
# Debug: Print configuration on startup
|
|
29
|
+
print(f"[Calendar MCP Server] ===== STARTING =====", file=sys.stderr)
|
|
30
|
+
print(f"[Calendar MCP Server] USER_ACCESS_TOKEN: {USER_ACCESS_TOKEN[:20] if USER_ACCESS_TOKEN else 'NONE'}...", file=sys.stderr)
|
|
31
|
+
print(f"[Calendar MCP Server] CALENDAR_API_URL: {CALENDAR_API_URL}", file=sys.stderr)
|
|
32
|
+
print(f"[Calendar MCP Server] ==================", file=sys.stderr)
|
|
33
|
+
sys.stderr.flush()
|
|
34
|
+
|
|
35
|
+
# Create FastMCP server
|
|
36
|
+
mcp = FastMCP("Calendar Client")
|
|
37
|
+
|
|
38
|
+
_http_client: Optional[httpx.AsyncClient] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_auth_headers() -> Dict[str, str]:
|
|
42
|
+
"""Get authentication headers with access token from env."""
|
|
43
|
+
headers = {"Content-Type": "application/json"}
|
|
44
|
+
if USER_ACCESS_TOKEN:
|
|
45
|
+
headers["Authorization"] = f"Bearer {USER_ACCESS_TOKEN}"
|
|
46
|
+
return headers
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def get_http() -> httpx.AsyncClient:
|
|
50
|
+
"""Get HTTP client with auth headers."""
|
|
51
|
+
global _http_client
|
|
52
|
+
|
|
53
|
+
if _http_client is None:
|
|
54
|
+
_http_client = httpx.AsyncClient(
|
|
55
|
+
timeout=30.0,
|
|
56
|
+
headers=_get_auth_headers()
|
|
57
|
+
)
|
|
58
|
+
return _http_client
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def resolve_calendar_id(calendar_id: Optional[str]) -> str:
|
|
62
|
+
"""Resolve 'primary' or missing calendar_id to the user's actual primary calendar id.
|
|
63
|
+
Preference order: id=='primary' -> primary flag true -> first item -> fallback 'primary'.
|
|
64
|
+
"""
|
|
65
|
+
target = (calendar_id or "primary").strip()
|
|
66
|
+
if target and target != "primary":
|
|
67
|
+
return target
|
|
68
|
+
try:
|
|
69
|
+
client = await get_http()
|
|
70
|
+
resp = await client.get(f"{CALENDAR_API_BASE}/users/me/calendarList", headers=_get_auth_headers())
|
|
71
|
+
if resp.status_code == 200:
|
|
72
|
+
data = resp.json() or {}
|
|
73
|
+
items = data.get("items", []) or []
|
|
74
|
+
for cal in items:
|
|
75
|
+
if cal.get("id") == "primary":
|
|
76
|
+
return "primary"
|
|
77
|
+
for cal in items:
|
|
78
|
+
if cal.get("primary"):
|
|
79
|
+
return cal.get("id")
|
|
80
|
+
if items:
|
|
81
|
+
return items[0].get("id") or "primary"
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return "primary"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@mcp.tool()
|
|
88
|
+
async def list_calendars(
|
|
89
|
+
min_access_role: Optional[str] = None,
|
|
90
|
+
show_deleted: bool = False,
|
|
91
|
+
show_hidden: bool = False,
|
|
92
|
+
) -> str:
|
|
93
|
+
"""List all calendars accessible to the user.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
min_access_role: Minimum access role (owner, writer, reader). Default: None (all)
|
|
97
|
+
show_deleted: Include deleted calendars. Default: False
|
|
98
|
+
show_hidden: Include hidden calendars. Default: False
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
JSON object with list of calendars
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
client = await get_http()
|
|
105
|
+
params = {}
|
|
106
|
+
if min_access_role:
|
|
107
|
+
params["minAccessRole"] = min_access_role
|
|
108
|
+
if show_deleted:
|
|
109
|
+
params["showDeleted"] = "true"
|
|
110
|
+
if show_hidden:
|
|
111
|
+
params["showHidden"] = "true"
|
|
112
|
+
|
|
113
|
+
resp = await client.get(
|
|
114
|
+
f"{CALENDAR_API_BASE}/users/me/calendarList",
|
|
115
|
+
params=params,
|
|
116
|
+
headers=_get_auth_headers()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if resp.status_code != 200:
|
|
120
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
121
|
+
|
|
122
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return json.dumps({"error": str(e)})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool()
|
|
128
|
+
async def list_events(
|
|
129
|
+
calendar_id: str = "primary",
|
|
130
|
+
time_min: Optional[str] = None,
|
|
131
|
+
time_max: Optional[str] = None,
|
|
132
|
+
max_results: int = 50,
|
|
133
|
+
order_by: str = "startTime",
|
|
134
|
+
single_events: bool = True,
|
|
135
|
+
show_deleted: bool = False,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""List events from a calendar.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
141
|
+
time_min: Lower bound (RFC3339 timestamp or relative like "now", "today")
|
|
142
|
+
time_max: Upper bound (RFC3339 timestamp or relative)
|
|
143
|
+
max_results: Maximum number of events (default: 50, max: 250)
|
|
144
|
+
order_by: Order of events ("startTime" or "updated")
|
|
145
|
+
single_events: Expand recurring events into instances (default: True)
|
|
146
|
+
show_deleted: Include deleted events (default: False)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
JSON object with list of events
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
client = await get_http()
|
|
153
|
+
params = {
|
|
154
|
+
"maxResults": min(max_results, 250),
|
|
155
|
+
"orderBy": order_by,
|
|
156
|
+
"singleEvents": str(single_events).lower(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if time_min:
|
|
160
|
+
params["timeMin"] = _parse_time_param(time_min)
|
|
161
|
+
if time_max:
|
|
162
|
+
params["timeMax"] = _parse_time_param(time_max)
|
|
163
|
+
if show_deleted:
|
|
164
|
+
params["showDeleted"] = "true"
|
|
165
|
+
|
|
166
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
167
|
+
|
|
168
|
+
# Basic retry loop for transient 5xx errors so agents don't get stuck
|
|
169
|
+
# on occasional backend hiccups during evaluation.
|
|
170
|
+
resp: httpx.Response | None = None
|
|
171
|
+
for attempt in range(3):
|
|
172
|
+
resp = await client.get(
|
|
173
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events",
|
|
174
|
+
params=params,
|
|
175
|
+
headers=_get_auth_headers(),
|
|
176
|
+
)
|
|
177
|
+
# Retry only on 5xx; anything else return immediately
|
|
178
|
+
if 500 <= resp.status_code < 600:
|
|
179
|
+
# Log details for debugging calendar sandbox 5xx
|
|
180
|
+
try:
|
|
181
|
+
print(
|
|
182
|
+
f"[Calendar MCP] list_events 5xx (attempt {attempt+1}) "
|
|
183
|
+
f"calendar_id={resolved_id!r} status={resp.status_code} body={resp.text[:300]}",
|
|
184
|
+
file=sys.stderr,
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
# Small backoff before retrying
|
|
189
|
+
if attempt < 2:
|
|
190
|
+
await asyncio.sleep(0.5 * (attempt + 1))
|
|
191
|
+
continue
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
if resp is None:
|
|
195
|
+
return json.dumps({"error": "Failed to contact calendar service"}, ensure_ascii=False)
|
|
196
|
+
|
|
197
|
+
if resp.status_code != 200:
|
|
198
|
+
# Keep the error structured but avoid crashing the agent; caller
|
|
199
|
+
# can treat this as "no events available" plus an error message.
|
|
200
|
+
return json.dumps(
|
|
201
|
+
{
|
|
202
|
+
"error": f"HTTP {resp.status_code}: {resp.text}",
|
|
203
|
+
"items": [],
|
|
204
|
+
},
|
|
205
|
+
ensure_ascii=False,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
return json.dumps({"error": str(e)})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.tool()
|
|
214
|
+
async def get_event(
|
|
215
|
+
event_id: str,
|
|
216
|
+
calendar_id: str = "primary",
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Get details of a specific event.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
event_id: Event identifier (REQUIRED)
|
|
222
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
JSON object with event details
|
|
226
|
+
"""
|
|
227
|
+
if not event_id:
|
|
228
|
+
return json.dumps({"error": "event_id is required"})
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
client = await get_http()
|
|
232
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
233
|
+
resp = await client.get(
|
|
234
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events/{event_id}",
|
|
235
|
+
headers=_get_auth_headers()
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if resp.status_code == 404:
|
|
239
|
+
return json.dumps({"error": f"Event {event_id} not found"})
|
|
240
|
+
if resp.status_code != 200:
|
|
241
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
242
|
+
|
|
243
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return json.dumps({"error": str(e)})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _parse_list_param(val: Any) -> Optional[List[str]]:
|
|
249
|
+
"""Parse a parameter that should be a list but might be a JSON string."""
|
|
250
|
+
if val is None:
|
|
251
|
+
return None
|
|
252
|
+
if isinstance(val, list):
|
|
253
|
+
return val
|
|
254
|
+
if isinstance(val, str):
|
|
255
|
+
val = val.strip()
|
|
256
|
+
if val.startswith('['):
|
|
257
|
+
try:
|
|
258
|
+
parsed = json.loads(val)
|
|
259
|
+
if isinstance(parsed, list):
|
|
260
|
+
return parsed
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
# Single email string
|
|
264
|
+
if '@' in val:
|
|
265
|
+
return [val]
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
async def create_event(
|
|
271
|
+
summary: str,
|
|
272
|
+
start_datetime: Optional[str] = None,
|
|
273
|
+
end_datetime: Optional[str] = None,
|
|
274
|
+
start_date: Optional[str] = None,
|
|
275
|
+
end_date: Optional[str] = None,
|
|
276
|
+
calendar_id: str = "primary",
|
|
277
|
+
description: Optional[str] = None,
|
|
278
|
+
location: Optional[str] = None,
|
|
279
|
+
attendees: Optional[Any] = None,
|
|
280
|
+
timezone: str = "UTC",
|
|
281
|
+
send_updates: Optional[str] = None,
|
|
282
|
+
) -> str:
|
|
283
|
+
"""Create a new calendar event.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
summary: Event title (REQUIRED)
|
|
287
|
+
start_datetime: Start date-time (RFC3339, e.g., "2025-10-23T14:00:00")
|
|
288
|
+
end_datetime: End date-time (RFC3339)
|
|
289
|
+
start_date: Start date for all-day events (YYYY-MM-DD)
|
|
290
|
+
end_date: End date for all-day events (YYYY-MM-DD)
|
|
291
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
292
|
+
description: Event description
|
|
293
|
+
location: Event location
|
|
294
|
+
attendees: List of attendee email addresses (or JSON string)
|
|
295
|
+
timezone: Timezone for the event (default: "UTC")
|
|
296
|
+
send_updates: Send email updates ("all", "externalOnly", "none")
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
JSON object with created event details
|
|
300
|
+
|
|
301
|
+
Note: Either provide start_datetime/end_datetime OR start_date/end_date
|
|
302
|
+
"""
|
|
303
|
+
if not summary:
|
|
304
|
+
return json.dumps({"error": "summary is required"})
|
|
305
|
+
|
|
306
|
+
# Parse attendees if it's a string
|
|
307
|
+
attendees_list = _parse_list_param(attendees)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
event_data: Dict[str, Any] = {"summary": summary}
|
|
311
|
+
|
|
312
|
+
# Handle start/end times
|
|
313
|
+
if start_datetime and end_datetime:
|
|
314
|
+
event_data["start"] = {
|
|
315
|
+
"dateTime": start_datetime,
|
|
316
|
+
"timeZone": timezone
|
|
317
|
+
}
|
|
318
|
+
event_data["end"] = {
|
|
319
|
+
"dateTime": end_datetime,
|
|
320
|
+
"timeZone": timezone
|
|
321
|
+
}
|
|
322
|
+
elif start_date and end_date:
|
|
323
|
+
event_data["start"] = {"date": start_date}
|
|
324
|
+
event_data["end"] = {"date": end_date}
|
|
325
|
+
elif start_datetime:
|
|
326
|
+
# If only start provided, default to 1 hour duration
|
|
327
|
+
event_data["start"] = {
|
|
328
|
+
"dateTime": start_datetime,
|
|
329
|
+
"timeZone": timezone
|
|
330
|
+
}
|
|
331
|
+
end_dt = _add_hours(start_datetime, 1)
|
|
332
|
+
event_data["end"] = {
|
|
333
|
+
"dateTime": end_dt,
|
|
334
|
+
"timeZone": timezone
|
|
335
|
+
}
|
|
336
|
+
else:
|
|
337
|
+
return json.dumps({"error": "Either start_datetime/end_datetime or start_date/end_date is required"})
|
|
338
|
+
|
|
339
|
+
if description:
|
|
340
|
+
event_data["description"] = description
|
|
341
|
+
if location:
|
|
342
|
+
event_data["location"] = location
|
|
343
|
+
if attendees_list:
|
|
344
|
+
event_data["attendees"] = [{"email": email} for email in attendees_list]
|
|
345
|
+
|
|
346
|
+
client = await get_http()
|
|
347
|
+
params = {}
|
|
348
|
+
if send_updates:
|
|
349
|
+
params["sendUpdates"] = send_updates
|
|
350
|
+
|
|
351
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
352
|
+
resp = await client.post(
|
|
353
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events",
|
|
354
|
+
json=event_data,
|
|
355
|
+
params=params,
|
|
356
|
+
headers=_get_auth_headers()
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if resp.status_code not in (200, 201):
|
|
360
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
361
|
+
|
|
362
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
return json.dumps({"error": str(e)})
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@mcp.tool()
|
|
368
|
+
async def update_event(
|
|
369
|
+
event_id: str,
|
|
370
|
+
calendar_id: str = "primary",
|
|
371
|
+
summary: Optional[str] = None,
|
|
372
|
+
start_datetime: Optional[str] = None,
|
|
373
|
+
end_datetime: Optional[str] = None,
|
|
374
|
+
start_date: Optional[str] = None,
|
|
375
|
+
end_date: Optional[str] = None,
|
|
376
|
+
description: Optional[str] = None,
|
|
377
|
+
location: Optional[str] = None,
|
|
378
|
+
attendees: Optional[List[str]] = None,
|
|
379
|
+
timezone: Optional[str] = None,
|
|
380
|
+
status: Optional[str] = None,
|
|
381
|
+
send_updates: Optional[str] = None,
|
|
382
|
+
) -> str:
|
|
383
|
+
"""Update an existing calendar event.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
event_id: Event identifier (REQUIRED)
|
|
387
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
388
|
+
summary: New event title
|
|
389
|
+
start_datetime: New start date-time (RFC3339)
|
|
390
|
+
end_datetime: New end date-time (RFC3339)
|
|
391
|
+
start_date: New start date for all-day events (YYYY-MM-DD)
|
|
392
|
+
end_date: New end date for all-day events (YYYY-MM-DD)
|
|
393
|
+
description: New event description
|
|
394
|
+
location: New event location
|
|
395
|
+
attendees: New list of attendee email addresses
|
|
396
|
+
timezone: New timezone
|
|
397
|
+
status: Event status ("confirmed", "tentative", "cancelled")
|
|
398
|
+
send_updates: Send email updates ("all", "externalOnly", "none")
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
JSON object with updated event details
|
|
402
|
+
"""
|
|
403
|
+
if not event_id:
|
|
404
|
+
return json.dumps({"error": "event_id is required"})
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
# First get the existing event
|
|
408
|
+
client = await get_http()
|
|
409
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
410
|
+
get_resp = await client.get(
|
|
411
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events/{event_id}",
|
|
412
|
+
headers=_get_auth_headers()
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if get_resp.status_code == 404:
|
|
416
|
+
return json.dumps({"error": f"Event {event_id} not found"})
|
|
417
|
+
if get_resp.status_code != 200:
|
|
418
|
+
return json.dumps({"error": f"HTTP {get_resp.status_code}: {get_resp.text}"})
|
|
419
|
+
|
|
420
|
+
event_data = get_resp.json()
|
|
421
|
+
|
|
422
|
+
# Update fields
|
|
423
|
+
if summary is not None:
|
|
424
|
+
event_data["summary"] = summary
|
|
425
|
+
if description is not None:
|
|
426
|
+
event_data["description"] = description
|
|
427
|
+
if location is not None:
|
|
428
|
+
event_data["location"] = location
|
|
429
|
+
if status is not None:
|
|
430
|
+
event_data["status"] = status
|
|
431
|
+
|
|
432
|
+
# Update start/end times
|
|
433
|
+
if start_datetime and end_datetime:
|
|
434
|
+
tz = timezone or event_data.get("start", {}).get("timeZone", "UTC")
|
|
435
|
+
event_data["start"] = {"dateTime": start_datetime, "timeZone": tz}
|
|
436
|
+
event_data["end"] = {"dateTime": end_datetime, "timeZone": tz}
|
|
437
|
+
elif start_date and end_date:
|
|
438
|
+
event_data["start"] = {"date": start_date}
|
|
439
|
+
event_data["end"] = {"date": end_date}
|
|
440
|
+
|
|
441
|
+
if attendees is not None:
|
|
442
|
+
event_data["attendees"] = [{"email": email} for email in attendees]
|
|
443
|
+
|
|
444
|
+
params = {}
|
|
445
|
+
if send_updates:
|
|
446
|
+
params["sendUpdates"] = send_updates
|
|
447
|
+
|
|
448
|
+
resp = await client.put(
|
|
449
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events/{event_id}",
|
|
450
|
+
json=event_data,
|
|
451
|
+
params=params,
|
|
452
|
+
headers=_get_auth_headers()
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if resp.status_code != 200:
|
|
456
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
457
|
+
|
|
458
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
return json.dumps({"error": str(e)})
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@mcp.tool()
|
|
464
|
+
async def delete_event(
|
|
465
|
+
event_id: str,
|
|
466
|
+
calendar_id: str = "primary",
|
|
467
|
+
send_updates: Optional[str] = None,
|
|
468
|
+
) -> str:
|
|
469
|
+
"""Delete a calendar event.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
event_id: Event identifier (REQUIRED)
|
|
473
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
474
|
+
send_updates: Send email updates ("all", "externalOnly", "none")
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
JSON object with deletion status
|
|
478
|
+
"""
|
|
479
|
+
if not event_id:
|
|
480
|
+
return json.dumps({"error": "event_id is required"})
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
client = await get_http()
|
|
484
|
+
params = {}
|
|
485
|
+
if send_updates:
|
|
486
|
+
params["sendUpdates"] = send_updates
|
|
487
|
+
|
|
488
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
489
|
+
resp = await client.delete(
|
|
490
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events/{event_id}",
|
|
491
|
+
params=params,
|
|
492
|
+
headers=_get_auth_headers()
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if resp.status_code in (200, 204, 404): # 404 is idempotent
|
|
496
|
+
return json.dumps({"ok": True, "event_id": event_id})
|
|
497
|
+
|
|
498
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
499
|
+
except Exception as e:
|
|
500
|
+
return json.dumps({"error": str(e)})
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@mcp.tool()
|
|
504
|
+
async def search_events(
|
|
505
|
+
query: str,
|
|
506
|
+
calendar_id: str = "primary",
|
|
507
|
+
time_min: Optional[str] = None,
|
|
508
|
+
time_max: Optional[str] = None,
|
|
509
|
+
max_results: int = 50,
|
|
510
|
+
) -> str:
|
|
511
|
+
"""Search for events by text query.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
query: Search query text (searches in summary, description, location, attendees)
|
|
515
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
516
|
+
time_min: Lower bound (RFC3339 timestamp or relative)
|
|
517
|
+
time_max: Upper bound (RFC3339 timestamp or relative)
|
|
518
|
+
max_results: Maximum number of results (default: 50, max: 250)
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
JSON object with matching events
|
|
522
|
+
"""
|
|
523
|
+
if not query:
|
|
524
|
+
return json.dumps({"error": "query is required"})
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
client = await get_http()
|
|
528
|
+
params = {
|
|
529
|
+
"q": query,
|
|
530
|
+
"maxResults": min(max_results, 250),
|
|
531
|
+
"singleEvents": "true"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if time_min:
|
|
535
|
+
params["timeMin"] = _parse_time_param(time_min)
|
|
536
|
+
if time_max:
|
|
537
|
+
params["timeMax"] = _parse_time_param(time_max)
|
|
538
|
+
|
|
539
|
+
resolved_id = await resolve_calendar_id(calendar_id)
|
|
540
|
+
resp = await client.get(
|
|
541
|
+
f"{CALENDAR_API_BASE}/calendars/{resolved_id}/events",
|
|
542
|
+
params=params,
|
|
543
|
+
headers=_get_auth_headers()
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if resp.status_code != 200:
|
|
547
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
548
|
+
|
|
549
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
550
|
+
except Exception as e:
|
|
551
|
+
return json.dumps({"error": str(e)})
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@mcp.tool()
|
|
555
|
+
async def get_freebusy(
|
|
556
|
+
time_min: str,
|
|
557
|
+
time_max: str,
|
|
558
|
+
calendar_ids: Optional[List[str]] = None,
|
|
559
|
+
timezone: str = "UTC",
|
|
560
|
+
) -> str:
|
|
561
|
+
"""Query free/busy information for calendars.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
time_min: Start of the interval (RFC3339 timestamp, REQUIRED)
|
|
565
|
+
time_max: End of the interval (RFC3339 timestamp, REQUIRED)
|
|
566
|
+
calendar_ids: List of calendar IDs to query (default: ["primary"])
|
|
567
|
+
timezone: Timezone for the query (default: "UTC")
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
JSON object with free/busy information
|
|
571
|
+
"""
|
|
572
|
+
if not time_min or not time_max:
|
|
573
|
+
return json.dumps({"error": "time_min and time_max are required"})
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
if calendar_ids is None:
|
|
577
|
+
calendar_ids = ["primary"]
|
|
578
|
+
|
|
579
|
+
request_body = {
|
|
580
|
+
"timeMin": _parse_time_param(time_min),
|
|
581
|
+
"timeMax": _parse_time_param(time_max),
|
|
582
|
+
"timeZone": timezone,
|
|
583
|
+
"items": [{"id": cal_id} for cal_id in calendar_ids]
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
client = await get_http()
|
|
587
|
+
resp = await client.post(
|
|
588
|
+
f"{CALENDAR_API_BASE}/calendar/v3/freeBusy",
|
|
589
|
+
json=request_body,
|
|
590
|
+
headers=_get_auth_headers()
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if resp.status_code != 200:
|
|
594
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
595
|
+
|
|
596
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
597
|
+
except Exception as e:
|
|
598
|
+
return json.dumps({"error": str(e)})
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@mcp.tool()
|
|
602
|
+
async def list_colors() -> str:
|
|
603
|
+
"""List available colors for calendars and events.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
JSON object with available color definitions
|
|
609
|
+
"""
|
|
610
|
+
try:
|
|
611
|
+
client = await get_http()
|
|
612
|
+
resp = await client.get(
|
|
613
|
+
f"{CALENDAR_API_BASE}/colors",
|
|
614
|
+
headers=_get_auth_headers()
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if resp.status_code != 200:
|
|
618
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
619
|
+
|
|
620
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
621
|
+
except Exception as e:
|
|
622
|
+
return json.dumps({"error": str(e)})
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@mcp.tool()
|
|
626
|
+
async def accept_invitation(
|
|
627
|
+
event_id: str,
|
|
628
|
+
calendar_id: str = "primary",
|
|
629
|
+
) -> str:
|
|
630
|
+
"""Accept a calendar event invitation.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
event_id: Event identifier (REQUIRED)
|
|
634
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
JSON object with updated event details
|
|
638
|
+
"""
|
|
639
|
+
if not event_id:
|
|
640
|
+
return json.dumps({"error": "event_id is required"})
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
client = await get_http()
|
|
644
|
+
resp = await client.post(
|
|
645
|
+
f"{CALENDAR_API_BASE}/events/{event_id}/accept",
|
|
646
|
+
headers=_get_auth_headers()
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if resp.status_code != 200:
|
|
650
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
651
|
+
|
|
652
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
653
|
+
except Exception as e:
|
|
654
|
+
return json.dumps({"error": str(e)})
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@mcp.tool()
|
|
658
|
+
async def decline_invitation(
|
|
659
|
+
event_id: str,
|
|
660
|
+
calendar_id: str = "primary",
|
|
661
|
+
) -> str:
|
|
662
|
+
"""Decline a calendar event invitation.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
event_id: Event identifier (REQUIRED)
|
|
666
|
+
calendar_id: Calendar identifier (default: "primary")
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
JSON object with updated event details
|
|
670
|
+
"""
|
|
671
|
+
if not event_id:
|
|
672
|
+
return json.dumps({"error": "event_id is required"})
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
client = await get_http()
|
|
676
|
+
resp = await client.post(
|
|
677
|
+
f"{CALENDAR_API_BASE}/events/{event_id}/decline",
|
|
678
|
+
headers=_get_auth_headers()
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
if resp.status_code != 200:
|
|
682
|
+
return json.dumps({"error": f"HTTP {resp.status_code}: {resp.text}"})
|
|
683
|
+
|
|
684
|
+
return json.dumps(resp.json(), ensure_ascii=False)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
return json.dumps({"error": str(e)})
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _parse_time_param(time_str: str) -> str:
|
|
690
|
+
"""Parse time parameter, handling relative times like 'now', 'today', etc."""
|
|
691
|
+
import re
|
|
692
|
+
|
|
693
|
+
raw = (time_str or "").strip()
|
|
694
|
+
key = raw.lower()
|
|
695
|
+
|
|
696
|
+
if key == "now":
|
|
697
|
+
return datetime.utcnow().isoformat() + "Z"
|
|
698
|
+
elif key == "today":
|
|
699
|
+
return (
|
|
700
|
+
datetime.utcnow()
|
|
701
|
+
.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
702
|
+
.isoformat()
|
|
703
|
+
+ "Z"
|
|
704
|
+
)
|
|
705
|
+
elif key == "tomorrow":
|
|
706
|
+
return (
|
|
707
|
+
(datetime.utcnow() + timedelta(days=1))
|
|
708
|
+
.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
709
|
+
.isoformat()
|
|
710
|
+
+ "Z"
|
|
711
|
+
)
|
|
712
|
+
elif key.startswith("+"):
|
|
713
|
+
# Handle relative times like "+1d", "+2h", "+30m"
|
|
714
|
+
try:
|
|
715
|
+
value = int(key[1:-1])
|
|
716
|
+
unit = key[-1]
|
|
717
|
+
if unit == "d":
|
|
718
|
+
dt = datetime.utcnow() + timedelta(days=value)
|
|
719
|
+
elif unit == "h":
|
|
720
|
+
dt = datetime.utcnow() + timedelta(hours=value)
|
|
721
|
+
elif unit == "m":
|
|
722
|
+
dt = datetime.utcnow() + timedelta(minutes=value)
|
|
723
|
+
else:
|
|
724
|
+
return raw
|
|
725
|
+
return dt.isoformat() + "Z"
|
|
726
|
+
except Exception:
|
|
727
|
+
return raw
|
|
728
|
+
else:
|
|
729
|
+
# Assume it's already a proper timestamp
|
|
730
|
+
if raw.endswith(("Z", "z")):
|
|
731
|
+
return raw[:-1] + "Z"
|
|
732
|
+
|
|
733
|
+
# Preserve explicit UTC offsets such as -05:00, +0000, -07, etc.
|
|
734
|
+
if re.search(r"(?:[+-]\d{2}:\d{2}|[+-]\d{4}|[+-]\d{2})$", raw):
|
|
735
|
+
return raw
|
|
736
|
+
|
|
737
|
+
# Some callers pass "...UTC"; normalize to RFC3339 Zulu.
|
|
738
|
+
if raw.upper().endswith("UTC"):
|
|
739
|
+
return raw[:-3] + "Z"
|
|
740
|
+
|
|
741
|
+
# Naive datetime/date strings: assume UTC.
|
|
742
|
+
return raw + "Z"
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _add_hours(datetime_str: str, hours: int) -> str:
|
|
746
|
+
"""Add hours to a datetime string."""
|
|
747
|
+
try:
|
|
748
|
+
# Remove timezone suffix for parsing
|
|
749
|
+
dt_str = datetime_str.rstrip("Z")
|
|
750
|
+
dt = datetime.fromisoformat(dt_str)
|
|
751
|
+
dt = dt + timedelta(hours=hours)
|
|
752
|
+
return dt.isoformat()
|
|
753
|
+
except Exception:
|
|
754
|
+
return datetime_str
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def main():
|
|
758
|
+
print("Starting Calendar MCP Server (Google Calendar Sandbox)...", file=sys.stderr)
|
|
759
|
+
sys.stderr.flush()
|
|
760
|
+
host = os.getenv("CALENDAR_MCP_HOST", "localhost")
|
|
761
|
+
env_port = os.getenv("PORT", "").strip()
|
|
762
|
+
|
|
763
|
+
def _port_from_registry(default_port: int) -> int:
|
|
764
|
+
"""Resolve port from registry.yaml as a static fallback."""
|
|
765
|
+
try:
|
|
766
|
+
if yaml is None:
|
|
767
|
+
return default_port
|
|
768
|
+
registry_path = Path(__file__).resolve().parent.parent / "registry.yaml"
|
|
769
|
+
if not registry_path.exists():
|
|
770
|
+
return default_port
|
|
771
|
+
data = yaml.safe_load(registry_path.read_text()) or {}
|
|
772
|
+
service_name = Path(__file__).resolve().parent.name # 'calendar'
|
|
773
|
+
for srv in (data.get("servers") or []):
|
|
774
|
+
if isinstance(srv, dict) and srv.get("name") == service_name:
|
|
775
|
+
env = srv.get("env") or {}
|
|
776
|
+
port_str = str(env.get("PORT") or "").strip().strip('\"')
|
|
777
|
+
return int(port_str) if port_str else default_port
|
|
778
|
+
except Exception:
|
|
779
|
+
return default_port
|
|
780
|
+
return default_port
|
|
781
|
+
|
|
782
|
+
if env_port.isdigit():
|
|
783
|
+
port = int(env_port)
|
|
784
|
+
else:
|
|
785
|
+
port = _port_from_registry(8841)
|
|
786
|
+
|
|
787
|
+
# FastMCP.run does not accept 'host' in some versions; bind via env if needed.
|
|
788
|
+
mcp.run(transport="http", port=port)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
if __name__ == "__main__":
|
|
792
|
+
main()
|