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.
Files changed (374) hide show
  1. agent/__init__.py +30 -0
  2. agent/claudesdk/__init__.py +8 -0
  3. agent/claudesdk/example.py +221 -0
  4. agent/claudesdk/src/__init__.py +8 -0
  5. agent/claudesdk/src/agent.py +400 -0
  6. agent/claudesdk/src/mcp_proxy.py +409 -0
  7. agent/claudesdk/src/utils.py +420 -0
  8. agent/googleadk/__init__.py +15 -0
  9. agent/googleadk/example.py +237 -0
  10. agent/googleadk/src/__init__.py +12 -0
  11. agent/googleadk/src/agent.py +401 -0
  12. agent/googleadk/src/mcp_wrapper.py +163 -0
  13. agent/googleadk/src/utils.py +602 -0
  14. agent/langchain/__init__.py +8 -0
  15. agent/langchain/example.py +213 -0
  16. agent/langchain/src/__init__.py +8 -0
  17. agent/langchain/src/agent.py +645 -0
  18. agent/langchain/src/utils.py +433 -0
  19. agent/openaisdk/__init__.py +17 -0
  20. agent/openaisdk/example.py +228 -0
  21. agent/openaisdk/src/__init__.py +12 -0
  22. agent/openaisdk/src/agent.py +491 -0
  23. agent/openaisdk/src/agent_wrapper.py +143 -0
  24. agent/openaisdk/src/mcp_wrapper.py +395 -0
  25. agent/openaisdk/src/utils.py +493 -0
  26. agent/openclaw/__init__.py +10 -0
  27. agent/openclaw/example.py +251 -0
  28. agent/openclaw/src/__init__.py +14 -0
  29. agent/openclaw/src/agent.py +930 -0
  30. agent/openclaw/src/helpers/__init__.py +1 -0
  31. agent/openclaw/src/helpers/auth_helpers.py +55 -0
  32. agent/openclaw/src/mcp_proxy.py +564 -0
  33. agent/openclaw/src/plugin_generator.py +231 -0
  34. agent/openclaw/src/utils.py +341 -0
  35. agent/pocketflow/__init__.py +18 -0
  36. agent/pocketflow/example.py +221 -0
  37. agent/pocketflow/prompts/react_agent.py +46 -0
  38. agent/pocketflow/src/__init__.py +6 -0
  39. agent/pocketflow/src/agent.py +507 -0
  40. agent/pocketflow/src/agent_wrapper.py +159 -0
  41. agent/pocketflow/src/async_helper.py +92 -0
  42. agent/pocketflow/src/mcp_react_agent.py +279 -0
  43. agent/pocketflow/src/native_agent.py +74 -0
  44. agent/pocketflow/src/nodes.py +467 -0
  45. benchmark/__init__.py +0 -0
  46. benchmark/browser/benign.jsonl +34 -0
  47. benchmark/browser/direct.jsonl +85 -0
  48. benchmark/browser/indirect.jsonl +82 -0
  49. benchmark/code/benign.jsonl +0 -0
  50. benchmark/code/direct.jsonl +121 -0
  51. benchmark/code/indirect.jsonl +165 -0
  52. benchmark/crm/benign.jsonl +165 -0
  53. benchmark/crm/direct.jsonl +90 -0
  54. benchmark/crm/indirect.jsonl +150 -0
  55. benchmark/customer-service/benign.jsonl +160 -0
  56. benchmark/customer-service/direct.jsonl +100 -0
  57. benchmark/customer-service/indirect.jsonl +101 -0
  58. benchmark/finance/benign.jsonl +0 -0
  59. benchmark/finance/direct.jsonl +200 -0
  60. benchmark/finance/indirect.jsonl +200 -0
  61. benchmark/legal/benign.jsonl +0 -0
  62. benchmark/legal/direct.jsonl +200 -0
  63. benchmark/legal/indirect.jsonl +200 -0
  64. benchmark/macos/benign.jsonl +30 -0
  65. benchmark/macos/direct.jsonl +50 -0
  66. benchmark/macos/indirect.jsonl +50 -0
  67. benchmark/medical/benign.jsonl +642 -0
  68. benchmark/medical/direct.jsonl +229 -0
  69. benchmark/medical/indirect.jsonl +222 -0
  70. benchmark/os-filesystem/benign.jsonl +200 -0
  71. benchmark/os-filesystem/direct.jsonl +200 -0
  72. benchmark/os-filesystem/indirect.jsonl +200 -0
  73. benchmark/research/benign.jsonl +0 -0
  74. benchmark/research/direct.jsonl +119 -0
  75. benchmark/research/indirect.jsonl +125 -0
  76. benchmark/telecom/benign.jsonl +120 -0
  77. benchmark/telecom/direct.jsonl +161 -0
  78. benchmark/telecom/indirect.jsonl +166 -0
  79. benchmark/travel/benign.jsonl +130 -0
  80. benchmark/travel/direct.jsonl +105 -0
  81. benchmark/travel/indirect.jsonl +120 -0
  82. benchmark/windows/benign.jsonl +100 -0
  83. benchmark/windows/direct.jsonl +140 -0
  84. benchmark/windows/indirect.jsonl +107 -0
  85. benchmark/workflow/benign.jsonl +335 -0
  86. benchmark/workflow/direct.jsonl +78 -0
  87. benchmark/workflow/indirect.jsonl +107 -0
  88. cli/__init__.py +5 -0
  89. cli/main.py +182 -0
  90. cli/scaffold.py +334 -0
  91. decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
  92. decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
  93. decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
  94. decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  95. decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
  96. decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
  97. dt_arena/config/env.yaml +515 -0
  98. dt_arena/config/injection_mcp.yaml +430 -0
  99. dt_arena/config/mcp.yaml +642 -0
  100. dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
  101. dt_arena/envs/arxiv/docker-compose.yml +36 -0
  102. dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
  103. dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
  104. dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
  105. dt_arena/envs/atlassian/docker-compose.yml +72 -0
  106. dt_arena/envs/bigquery/docker-compose.yml +20 -0
  107. dt_arena/envs/booking/docker-compose.yml +59 -0
  108. dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
  109. dt_arena/envs/calendar/docker-compose.yml +42 -0
  110. dt_arena/envs/custom-website/docker-compose.yml +6 -0
  111. dt_arena/envs/customer_service/docker-compose.yml +59 -0
  112. dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
  113. dt_arena/envs/databricks/docker-compose.yml +51 -0
  114. dt_arena/envs/ecommerce/docker-compose.yml +6 -0
  115. dt_arena/envs/ers/docker-compose.yml +36 -0
  116. dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
  117. dt_arena/envs/finance/docker-compose.yml +23 -0
  118. dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
  119. dt_arena/envs/github/docker/docker-compose.yml +50 -0
  120. dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
  121. dt_arena/envs/gmail/docker-compose.yml +65 -0
  122. dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
  123. dt_arena/envs/google-form/docker-compose.yml +41 -0
  124. dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
  125. dt_arena/envs/googledocs/docker-compose.yml +78 -0
  126. dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
  127. dt_arena/envs/hospital/docker-compose.yml +27 -0
  128. dt_arena/envs/legal/docker-compose.yml +22 -0
  129. dt_arena/envs/linkedin/docker-compose.yml +63 -0
  130. dt_arena/envs/macos/docker-compose.yml +79 -0
  131. dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
  132. dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
  133. dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
  134. dt_arena/envs/paypal/docker-compose.yml +63 -0
  135. dt_arena/envs/research/docker-compose-hub.yml +13 -0
  136. dt_arena/envs/research/docker-compose.yml +24 -0
  137. dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
  138. dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
  139. dt_arena/envs/slack/docker-compose-hub.yml +28 -0
  140. dt_arena/envs/slack/docker-compose.yml +41 -0
  141. dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
  142. dt_arena/envs/snowflake/docker-compose.yml +44 -0
  143. dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
  144. dt_arena/envs/telecom/docker-compose.yml +17 -0
  145. dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
  146. dt_arena/envs/telegram/docker-compose.yml +62 -0
  147. dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
  148. dt_arena/envs/terminal/docker-compose.yml +26 -0
  149. dt_arena/envs/travel/docker-compose-hub.yml +19 -0
  150. dt_arena/envs/travel/docker-compose.yml +19 -0
  151. dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
  152. dt_arena/envs/whatsapp/docker-compose.yml +78 -0
  153. dt_arena/envs/windows/docker-compose.yml +71 -0
  154. dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
  155. dt_arena/envs/zoom/docker-compose.yml +40 -0
  156. dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
  157. dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
  158. dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
  159. dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
  160. dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
  161. dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
  162. dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
  163. dt_arena/injection_mcp_server/github/env_injection.py +206 -0
  164. dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
  165. dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
  166. dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
  167. dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
  168. dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
  169. dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
  170. dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
  171. dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
  172. dt_arena/injection_mcp_server/research/env_injection.py +616 -0
  173. dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
  174. dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
  175. dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
  176. dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
  177. dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
  178. dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
  179. dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
  180. dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
  181. dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
  182. dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
  183. dt_arena/mcp_server/atlassian/main.py +1554 -0
  184. dt_arena/mcp_server/atlassian/test_server.py +66 -0
  185. dt_arena/mcp_server/bigquery/main.py +333 -0
  186. dt_arena/mcp_server/booking/main.py +310 -0
  187. dt_arena/mcp_server/browser/main.py +1741 -0
  188. dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
  189. dt_arena/mcp_server/calendar/main.py +792 -0
  190. dt_arena/mcp_server/calendar/test_mcp.py +135 -0
  191. dt_arena/mcp_server/customer_service/main.py +1063 -0
  192. dt_arena/mcp_server/databricks/main.py +566 -0
  193. dt_arena/mcp_server/databricks/probe.py +102 -0
  194. dt_arena/mcp_server/ers/main.py +845 -0
  195. dt_arena/mcp_server/finance/__init__.py +87 -0
  196. dt_arena/mcp_server/finance/core/__init__.py +12 -0
  197. dt_arena/mcp_server/finance/core/data_loader.py +558 -0
  198. dt_arena/mcp_server/finance/core/portfolio.py +565 -0
  199. dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
  200. dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
  201. dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
  202. dt_arena/mcp_server/finance/injection/__init__.py +66 -0
  203. dt_arena/mcp_server/finance/injection/config.py +176 -0
  204. dt_arena/mcp_server/finance/injection/content.py +755 -0
  205. dt_arena/mcp_server/finance/injection/html.py +409 -0
  206. dt_arena/mcp_server/finance/injection/locations.py +167 -0
  207. dt_arena/mcp_server/finance/injection/methods.py +193 -0
  208. dt_arena/mcp_server/finance/injection/presets.py +1023 -0
  209. dt_arena/mcp_server/finance/main.py +361 -0
  210. dt_arena/mcp_server/finance/run_mcp.py +21 -0
  211. dt_arena/mcp_server/finance/run_web.py +26 -0
  212. dt_arena/mcp_server/finance/server/__init__.py +41 -0
  213. dt_arena/mcp_server/finance/server/extractor.py +1453 -0
  214. dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
  215. dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
  216. dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
  217. dt_arena/mcp_server/finance/server/mcp.py +451 -0
  218. dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
  219. dt_arena/mcp_server/finance/server/tools/account.py +88 -0
  220. dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
  221. dt_arena/mcp_server/finance/server/tools/social.py +73 -0
  222. dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
  223. dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
  224. dt_arena/mcp_server/finance/server/web.py +2139 -0
  225. dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
  226. dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
  227. dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
  228. dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
  229. dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
  230. dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
  231. dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
  232. dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
  233. dt_arena/mcp_server/github/main.py +441 -0
  234. dt_arena/mcp_server/gmail/main.py +1004 -0
  235. dt_arena/mcp_server/google_form/main.py +141 -0
  236. dt_arena/mcp_server/googledocs/main.py +458 -0
  237. dt_arena/mcp_server/hospital/mcp_server.py +458 -0
  238. dt_arena/mcp_server/legal/__init__.py +9 -0
  239. dt_arena/mcp_server/legal/core/__init__.py +14 -0
  240. dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
  241. dt_arena/mcp_server/legal/core/data_loader.py +266 -0
  242. dt_arena/mcp_server/legal/core/document_store.py +197 -0
  243. dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
  244. dt_arena/mcp_server/legal/main.py +89 -0
  245. dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
  246. dt_arena/mcp_server/legal/server/__init__.py +14 -0
  247. dt_arena/mcp_server/legal/server/mcp.py +2330 -0
  248. dt_arena/mcp_server/macos/client_test.py +270 -0
  249. dt_arena/mcp_server/macos/mcp_server.py +285 -0
  250. dt_arena/mcp_server/os-filesystem/main.py +1380 -0
  251. dt_arena/mcp_server/paypal/main.py +501 -0
  252. dt_arena/mcp_server/research/main.py +777 -0
  253. dt_arena/mcp_server/salesforce/main.py +2006 -0
  254. dt_arena/mcp_server/slack/main.py +318 -0
  255. dt_arena/mcp_server/snowflake/main.py +612 -0
  256. dt_arena/mcp_server/snowflake/probe.py +183 -0
  257. dt_arena/mcp_server/telecom/mcp_client.py +423 -0
  258. dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
  259. dt_arena/mcp_server/telegram/main.py +338 -0
  260. dt_arena/mcp_server/terminal/main.py +163 -0
  261. dt_arena/mcp_server/travel/client_test.py +16 -0
  262. dt_arena/mcp_server/travel/mcp_server.py +404 -0
  263. dt_arena/mcp_server/whatsapp/main.py +318 -0
  264. dt_arena/mcp_server/windows/client_test.py +270 -0
  265. dt_arena/mcp_server/windows/mcp_server.py +218 -0
  266. dt_arena/mcp_server/zoom/main.py +466 -0
  267. dt_arena/src/__init__.py +0 -0
  268. dt_arena/src/hooks/__init__.py +0 -0
  269. dt_arena/src/hooks/audit_log.py +30 -0
  270. dt_arena/src/hooks/hooks.json +3 -0
  271. dt_arena/src/run_benign.py +142 -0
  272. dt_arena/src/types/__init__.py +0 -0
  273. dt_arena/src/types/agent.py +441 -0
  274. dt_arena/src/types/attacks.py +2 -0
  275. dt_arena/src/types/environment.py +2 -0
  276. dt_arena/src/types/hooks.py +174 -0
  277. dt_arena/src/types/judge.py +52 -0
  278. dt_arena/src/types/red_teaming_trajectory.py +385 -0
  279. dt_arena/src/types/task.py +260 -0
  280. dt_arena/src/types/trajectory.py +315 -0
  281. dt_arena/utils/__init__.py +1 -0
  282. dt_arena/utils/atlassian/__init__.py +27 -0
  283. dt_arena/utils/atlassian/helpers.py +520 -0
  284. dt_arena/utils/bigquery/__init__.py +1 -0
  285. dt_arena/utils/bigquery/helpers.py +246 -0
  286. dt_arena/utils/calendar/__init__.py +1 -0
  287. dt_arena/utils/calendar/helpers.py +87 -0
  288. dt_arena/utils/customer_service/__init__.py +17 -0
  289. dt_arena/utils/customer_service/cs_env_client.py +940 -0
  290. dt_arena/utils/customer_service/helpers.py +339 -0
  291. dt_arena/utils/customer_service/judges/__init__.py +20 -0
  292. dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
  293. dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
  294. dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
  295. dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
  296. dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
  297. dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
  298. dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
  299. dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
  300. dt_arena/utils/customer_service/judges/text_utils.py +21 -0
  301. dt_arena/utils/databricks/__init__.py +2 -0
  302. dt_arena/utils/databricks/helpers.py +210 -0
  303. dt_arena/utils/finance/__init__.py +0 -0
  304. dt_arena/utils/finance/helpers.py +263 -0
  305. dt_arena/utils/github/__init__.py +1 -0
  306. dt_arena/utils/github/helpers.py +249 -0
  307. dt_arena/utils/gmail/__init__.py +1 -0
  308. dt_arena/utils/gmail/helpers.py +344 -0
  309. dt_arena/utils/google_form/__init__.py +2 -0
  310. dt_arena/utils/google_form/helpers.py +133 -0
  311. dt_arena/utils/legal/__init__.py +0 -0
  312. dt_arena/utils/legal/helpers.py +228 -0
  313. dt_arena/utils/macos/__init__.py +0 -0
  314. dt_arena/utils/macos/env_setup.py +215 -0
  315. dt_arena/utils/macos/helpers.py +61 -0
  316. dt_arena/utils/os_filesystem/__init__.py +1 -0
  317. dt_arena/utils/os_filesystem/helpers.py +366 -0
  318. dt_arena/utils/paypal/__init__.py +1 -0
  319. dt_arena/utils/paypal/helpers.py +178 -0
  320. dt_arena/utils/port_allocator.py +266 -0
  321. dt_arena/utils/research/__init__.py +0 -0
  322. dt_arena/utils/research/helpers.py +251 -0
  323. dt_arena/utils/salesforce/__init__.py +1 -0
  324. dt_arena/utils/salesforce/helpers.py +719 -0
  325. dt_arena/utils/slack/__init__.py +1 -0
  326. dt_arena/utils/slack/helpers.py +176 -0
  327. dt_arena/utils/snowflake/__init__.py +1 -0
  328. dt_arena/utils/snowflake/helpers.py +166 -0
  329. dt_arena/utils/telecom/__init__.py +1 -0
  330. dt_arena/utils/telecom/helpers.py +760 -0
  331. dt_arena/utils/telegram/__init__.py +0 -0
  332. dt_arena/utils/telegram/helpers.py +174 -0
  333. dt_arena/utils/terminal/__init__.py +0 -0
  334. dt_arena/utils/terminal/helpers.py +20 -0
  335. dt_arena/utils/travel/__init__.py +0 -0
  336. dt_arena/utils/travel/env_client.py +537 -0
  337. dt_arena/utils/travel/llm_judge.py +137 -0
  338. dt_arena/utils/travel/prompts.py +64 -0
  339. dt_arena/utils/utils/__init__.py +122 -0
  340. dt_arena/utils/whatsapp/__init__.py +0 -0
  341. dt_arena/utils/whatsapp/helpers.py +226 -0
  342. dt_arena/utils/windows/__init__.py +0 -0
  343. dt_arena/utils/windows/env_reset.py +224 -0
  344. dt_arena/utils/windows/env_setup.py +280 -0
  345. dt_arena/utils/windows/exfil_helpers.py +170 -0
  346. dt_arena/utils/windows/helpers.py +74 -0
  347. dt_arena/utils/zoom/__init__.py +1 -0
  348. dt_arena/utils/zoom/helpers.py +70 -0
  349. eval/__init__.py +1 -0
  350. eval/evaluation.py +426 -0
  351. eval/task_runner.py +449 -0
  352. utils/__init__.py +148 -0
  353. utils/agent_helpers.py +308 -0
  354. utils/agent_wrapper.py +189 -0
  355. utils/compose_utils.py +135 -0
  356. utils/config.py +77 -0
  357. utils/env_helpers.py +104 -0
  358. utils/eval_stats.py +88 -0
  359. utils/injection_helpers.py +429 -0
  360. utils/injection_mcp_helpers.py +152 -0
  361. utils/judge_helpers.py +181 -0
  362. utils/judge_utils.py +472 -0
  363. utils/llm.py +196 -0
  364. utils/logging.py +45 -0
  365. utils/mcp_helpers.py +232 -0
  366. utils/mcp_manager.py +235 -0
  367. utils/memory_guard.py +18 -0
  368. utils/red_teaming_sandbox.py +476 -0
  369. utils/reset_helpers.py +318 -0
  370. utils/resource_manager.py +370 -0
  371. utils/skill_helpers.py +447 -0
  372. utils/task_executor.py +904 -0
  373. utils/task_helpers.py +270 -0
  374. 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()