ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
parrot/mcp/oauth.py ADDED
@@ -0,0 +1,933 @@
1
+ import os
2
+ import sys
3
+ from typing import Optional, Dict, Any
4
+ from dataclasses import dataclass, field
5
+ import asyncio
6
+ import time
7
+ import base64
8
+ import hashlib
9
+ import secrets
10
+ import json
11
+ from urllib.parse import urlencode
12
+ from aiohttp import web, ClientSession
13
+
14
+
15
+ def _b64url(data: bytes) -> str:
16
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
17
+
18
+ def _now() -> int:
19
+ return int(time.time())
20
+
21
+
22
+ # ---- API Key Authentication ----
23
+
24
+ @dataclass
25
+ class APIKeyRecord:
26
+ """Record for an issued API key."""
27
+ key: str
28
+ user_id: str
29
+ created_at: float
30
+ expires_at: Optional[float] = None
31
+ scopes: list[str] = field(default_factory=list)
32
+ description: str = ""
33
+
34
+
35
+ class APIKeyStore:
36
+ """
37
+ In-memory API key store with session logging.
38
+
39
+ Provides API key issuance, validation, and session tracking for
40
+ MCP server authentication.
41
+ """
42
+
43
+ def __init__(self):
44
+ self._keys: Dict[str, APIKeyRecord] = {}
45
+ self._sessions: list[Dict[str, Any]] = []
46
+
47
+ def issue_key(
48
+ self,
49
+ user_id: str,
50
+ scopes: Optional[list[str]] = None,
51
+ ttl: Optional[int] = None,
52
+ description: str = ""
53
+ ) -> APIKeyRecord:
54
+ """
55
+ Issue a new API key for a user.
56
+
57
+ Args:
58
+ user_id: User identifier
59
+ scopes: Optional list of scopes for the key
60
+ ttl: Time-to-live in seconds (None for no expiration)
61
+ description: Human-readable description
62
+
63
+ Returns:
64
+ APIKeyRecord with the issued key
65
+ """
66
+ key = f"mcp_key_{secrets.token_urlsafe(32)}"
67
+ now = _now()
68
+ expires_at = (now + ttl) if ttl else None
69
+
70
+ record = APIKeyRecord(
71
+ key=key,
72
+ user_id=user_id,
73
+ created_at=now,
74
+ expires_at=expires_at,
75
+ scopes=scopes or [],
76
+ description=description,
77
+ )
78
+ self._keys[key] = record
79
+ return record
80
+
81
+ def validate_key(self, key: str) -> Optional[APIKeyRecord]:
82
+ """
83
+ Validate an API key.
84
+
85
+ Args:
86
+ key: The API key to validate
87
+
88
+ Returns:
89
+ APIKeyRecord if valid, None if invalid or expired
90
+ """
91
+ if not key:
92
+ return None
93
+
94
+ record = self._keys.get(key)
95
+ if not record:
96
+ return None
97
+
98
+ # Check expiration
99
+ if record.expires_at and record.expires_at <= _now():
100
+ return None
101
+
102
+ return record
103
+
104
+ def revoke_key(self, key: str) -> bool:
105
+ """
106
+ Revoke an API key.
107
+
108
+ Args:
109
+ key: The API key to revoke
110
+
111
+ Returns:
112
+ True if revoked, False if key not found
113
+ """
114
+ if key in self._keys:
115
+ del self._keys[key]
116
+ return True
117
+ return False
118
+
119
+ def log_session_start(self, key: str, user_id: str, timestamp: float) -> None:
120
+ """
121
+ Log the start of a session using an API key.
122
+
123
+ Args:
124
+ key: The API key used
125
+ user_id: User identifier
126
+ timestamp: Session start timestamp
127
+ """
128
+ self._sessions.append({
129
+ "key": key[:16] + "...", # Truncate for security
130
+ "user_id": user_id,
131
+ "started_at": timestamp,
132
+ "started_at_iso": time.strftime(
133
+ "%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp)
134
+ ),
135
+ })
136
+
137
+ def get_sessions(
138
+ self, user_id: Optional[str] = None, limit: int = 100
139
+ ) -> list[Dict[str, Any]]:
140
+ """
141
+ Get session logs.
142
+
143
+ Args:
144
+ user_id: Optional filter by user ID
145
+ limit: Maximum number of sessions to return
146
+
147
+ Returns:
148
+ List of session records
149
+ """
150
+ sessions = self._sessions
151
+ if user_id:
152
+ sessions = [s for s in sessions if s["user_id"] == user_id]
153
+ return sessions[-limit:]
154
+
155
+ def list_keys(self, user_id: Optional[str] = None) -> list[APIKeyRecord]:
156
+ """
157
+ List all API keys.
158
+
159
+ Args:
160
+ user_id: Optional filter by user ID
161
+
162
+ Returns:
163
+ List of API key records
164
+ """
165
+ keys = list(self._keys.values())
166
+ if user_id:
167
+ keys = [k for k in keys if k.user_id == user_id]
168
+ return keys
169
+
170
+
171
+ # ---- External OAuth2 Integration ----
172
+
173
+ class ExternalOAuthValidator:
174
+ """
175
+ Validates tokens against external OAuth2 servers using RFC 7662 introspection.
176
+
177
+ Use this for integrating with external identity providers like Azure AD,
178
+ Keycloak, Okta, etc.
179
+ """
180
+
181
+ def __init__(
182
+ self,
183
+ introspection_endpoint: str,
184
+ client_id: str,
185
+ client_secret: str,
186
+ resource_server_url: Optional[str] = None,
187
+ http_timeout: float = 15.0,
188
+ ):
189
+ """
190
+ Initialize external OAuth validator.
191
+
192
+ Args:
193
+ introspection_endpoint: Token introspection endpoint URL
194
+ client_id: Client ID for introspection requests
195
+ client_secret: Client secret for introspection requests
196
+ resource_server_url: Expected audience/resource URL
197
+ http_timeout: HTTP request timeout in seconds
198
+ """
199
+ self.introspection_endpoint = introspection_endpoint
200
+ self.client_id = client_id
201
+ self.client_secret = client_secret
202
+ self.resource_server_url = resource_server_url
203
+ self.http_timeout = http_timeout
204
+ self._token_cache: Dict[str, Dict[str, Any]] = {}
205
+
206
+ async def validate_token(self, token: str) -> Optional[Dict[str, Any]]:
207
+ """
208
+ Validate a token via introspection.
209
+
210
+ Args:
211
+ token: Bearer token to validate
212
+
213
+ Returns:
214
+ Token info dict if valid, None if invalid
215
+ """
216
+ if not token:
217
+ return None
218
+
219
+ try:
220
+ info = await self.get_token_info(token)
221
+ if not info.get("active", False):
222
+ return None
223
+
224
+ # Validate audience if configured
225
+ if self.resource_server_url:
226
+ aud = info.get("aud", [])
227
+ if isinstance(aud, str):
228
+ aud = [aud]
229
+ if self.resource_server_url not in aud:
230
+ return None
231
+
232
+ return info
233
+ except Exception:
234
+ return None
235
+
236
+ async def get_token_info(self, token: str) -> Dict[str, Any]:
237
+ """
238
+ Get token info from introspection endpoint (RFC 7662).
239
+
240
+ Args:
241
+ token: Bearer token to introspect
242
+
243
+ Returns:
244
+ Token introspection response
245
+
246
+ Raises:
247
+ Exception on HTTP or validation errors
248
+ """
249
+ # Check cache first
250
+ cached = self._token_cache.get(token)
251
+ if cached and cached.get("_cached_until", 0) > _now():
252
+ return cached
253
+
254
+ # Prepare introspection request
255
+ params = {
256
+ "token": token,
257
+ "client_id": self.client_id,
258
+ "client_secret": self.client_secret,
259
+ }
260
+
261
+ async with ClientSession() as session:
262
+ async with session.post(
263
+ self.introspection_endpoint,
264
+ data=params,
265
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
266
+ timeout=self.http_timeout,
267
+ ) as response:
268
+ if response.status != 200:
269
+ text = await response.text()
270
+ raise RuntimeError(
271
+ f"Introspection failed: {response.status} - {text}"
272
+ )
273
+
274
+ info = await response.json()
275
+
276
+ # Cache with TTL
277
+ if info.get("active"):
278
+ exp = info.get("exp", _now() + 60)
279
+ info["_cached_until"] = min(exp, _now() + 300) # Max 5 min cache
280
+ self._token_cache[token] = info
281
+
282
+ return info
283
+
284
+ def clear_cache(self) -> None:
285
+ """Clear the token cache."""
286
+ self._token_cache.clear()
287
+
288
+
289
+ # ---- OAuth Client Models ----
290
+
291
+ @dataclass
292
+ class OAuthClient:
293
+ client_id: str
294
+ client_secret: str
295
+ client_name: str
296
+ redirect_uris: list[str]
297
+ scopes: list[str] = field(default_factory=list)
298
+ created_at: float = field(default_factory=time.time)
299
+
300
+
301
+ class ClientRegistry:
302
+ """
303
+ Minimal in-memory Dynamic Client Registration (RFC 7591) registry.
304
+ Suitable for local development / proxy-style OAuth flows.
305
+ """
306
+
307
+ def __init__(self):
308
+ self._clients: Dict[str, OAuthClient] = {}
309
+
310
+ def register(self, metadata: Dict[str, Any]) -> OAuthClient:
311
+ if "redirect_uris" not in metadata:
312
+ raise ValueError("redirect_uris is required for client registration")
313
+
314
+ client_id = metadata.get("client_id") or secrets.token_urlsafe(16)
315
+ client_secret = metadata.get("client_secret") or secrets.token_urlsafe(32)
316
+ client_name = metadata.get("client_name") or metadata.get("client_name", "mcp-client")
317
+ redirect_uris = metadata["redirect_uris"]
318
+ scopes = metadata.get("scope", "") or metadata.get("scopes", [])
319
+ if isinstance(scopes, str):
320
+ scopes = scopes.split()
321
+
322
+ client = OAuthClient(
323
+ client_id=client_id,
324
+ client_secret=client_secret,
325
+ client_name=client_name,
326
+ redirect_uris=redirect_uris,
327
+ scopes=scopes,
328
+ )
329
+ self._clients[client_id] = client
330
+ return client
331
+
332
+ def get(self, client_id: str) -> Optional[OAuthClient]:
333
+ return self._clients.get(client_id)
334
+
335
+
336
+ class OAuthAuthorizationServer:
337
+ """In-memory OAuth 2.0 authorization server for MCP transports."""
338
+
339
+ def __init__(
340
+ self,
341
+ *,
342
+ default_scopes: Optional[list[str]] = None,
343
+ allow_dynamic_registration: bool = True,
344
+ token_ttl: int = 3600,
345
+ code_ttl: int = 600,
346
+ ):
347
+ self.registry = ClientRegistry()
348
+ self.default_scopes = default_scopes or ["mcp:access"]
349
+ self.allow_dynamic_registration = allow_dynamic_registration
350
+ self.token_ttl = token_ttl
351
+ self.code_ttl = code_ttl
352
+ self._codes: Dict[str, Dict[str, Any]] = {}
353
+ self._tokens: Dict[str, Dict[str, Any]] = {}
354
+
355
+ def register_routes(self, app: web.Application) -> None:
356
+ app.router.add_get("/.well-known/oauth-authorization-server", self._handle_discovery)
357
+ app.router.add_post("/oauth/register", self._handle_registration)
358
+ app.router.add_get("/oauth/authorize", self._handle_authorize)
359
+ app.router.add_post("/oauth/token", self._handle_token)
360
+
361
+ def bearer_token_from_header(self, header: Optional[str]) -> Optional[str]:
362
+ if not header:
363
+ return None
364
+ if not header.lower().startswith("bearer "):
365
+ return None
366
+ return header.split(" ", 1)[1].strip()
367
+
368
+ def is_token_valid(self, token: Optional[str]) -> bool:
369
+ if not token:
370
+ return False
371
+ stored = self._tokens.get(token)
372
+ if not stored:
373
+ return False
374
+ return stored.get("expires_at", 0) > _now()
375
+
376
+ def _build_base_url(self, request: web.Request) -> str:
377
+ return f"{request.scheme}://{request.host}"
378
+
379
+ async def _handle_discovery(self, request: web.Request) -> web.Response:
380
+ base_url = self._build_base_url(request)
381
+ metadata = {
382
+ "issuer": base_url,
383
+ "authorization_endpoint": f"{base_url}/oauth/authorize",
384
+ "token_endpoint": f"{base_url}/oauth/token",
385
+ "registration_endpoint": f"{base_url}/oauth/register",
386
+ "response_types_supported": ["code"],
387
+ "grant_types_supported": ["authorization_code"],
388
+ "code_challenge_methods_supported": ["S256"],
389
+ "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
390
+ "scopes_supported": self.default_scopes,
391
+ }
392
+ return web.json_response(metadata)
393
+
394
+ async def _handle_registration(self, request: web.Request) -> web.Response:
395
+ if not self.allow_dynamic_registration:
396
+ return web.json_response({"error": "registration_not_supported"}, status=400)
397
+
398
+ try:
399
+ data = await request.json()
400
+ except Exception:
401
+ return web.json_response({"error": "invalid_request"}, status=400)
402
+
403
+ try:
404
+ client = self.registry.register(data)
405
+ except Exception as exc: # pragma: no cover - defensive
406
+ return web.json_response(
407
+ {
408
+ "error": "invalid_client_metadata",
409
+ "error_description": str(exc),
410
+ },
411
+ status=400,
412
+ )
413
+
414
+ return web.json_response(
415
+ {
416
+ "client_id": client.client_id,
417
+ "client_secret": client.client_secret,
418
+ "client_id_issued_at": int(client.created_at),
419
+ "client_secret_expires_at": 0,
420
+ "client_name": client.client_name,
421
+ "redirect_uris": client.redirect_uris,
422
+ "scope": " ".join(client.scopes or self.default_scopes),
423
+ },
424
+ status=201,
425
+ )
426
+
427
+ async def _handle_authorize(self, request: web.Request) -> web.StreamResponse:
428
+ params = request.query
429
+ client_id = params.get("client_id")
430
+ redirect_uri = params.get("redirect_uri")
431
+ state = params.get("state")
432
+ response_type = params.get("response_type")
433
+ code_challenge = params.get("code_challenge")
434
+ code_challenge_method = params.get("code_challenge_method", "plain")
435
+
436
+ if response_type != "code":
437
+ return web.Response(status=400, text="unsupported response_type")
438
+
439
+ client = self.registry.get(client_id) if client_id else None
440
+ if not client:
441
+ return web.Response(status=400, text="Invalid Client ID")
442
+
443
+ if redirect_uri not in client.redirect_uris:
444
+ return web.Response(status=400, text="Invalid Redirect URI")
445
+
446
+ scopes = params.get("scope", "").split()
447
+ if not scopes:
448
+ scopes = client.scopes or self.default_scopes
449
+
450
+ code = self._issue_code(
451
+ client_id=client_id,
452
+ redirect_uri=redirect_uri,
453
+ scope=scopes,
454
+ code_challenge=code_challenge,
455
+ code_challenge_method=code_challenge_method,
456
+ )
457
+
458
+ target = f"{redirect_uri}?code={code}"
459
+ if state:
460
+ target += f"&state={state}"
461
+ return web.HTTPFound(target)
462
+
463
+ async def _handle_token(self, request: web.Request) -> web.Response:
464
+ data = await request.post()
465
+ grant_type = data.get("grant_type")
466
+ code = data.get("code")
467
+ client_id = data.get("client_id")
468
+
469
+ if grant_type != "authorization_code":
470
+ return web.json_response({"error": "unsupported_grant_type"}, status=400)
471
+
472
+ record = self._codes.pop(code, None)
473
+ if not record:
474
+ return web.json_response({"error": "invalid_grant"}, status=400)
475
+
476
+ if record["expires_at"] <= _now():
477
+ return web.json_response({"error": "invalid_grant"}, status=400)
478
+
479
+ if client_id != record["client_id"]:
480
+ return web.json_response({"error": "invalid_client"}, status=400)
481
+
482
+ if record.get("code_challenge"):
483
+ verifier = data.get("code_verifier")
484
+ if not verifier:
485
+ return web.json_response({"error": "invalid_request"}, status=400)
486
+ computed = _b64url(hashlib.sha256(verifier.encode()).digest())
487
+ if computed != record["code_challenge"]:
488
+ return web.json_response({"error": "invalid_grant"}, status=400)
489
+
490
+ token_payload = self._issue_token(client_id=client_id, scope=record["scope"])
491
+ return web.json_response(token_payload)
492
+
493
+ def _issue_code(
494
+ self,
495
+ *,
496
+ client_id: str,
497
+ redirect_uri: str,
498
+ scope: list[str],
499
+ code_challenge: Optional[str],
500
+ code_challenge_method: Optional[str],
501
+ ) -> str:
502
+ code = f"auth_code_{secrets.token_urlsafe(10)}"
503
+ self._codes[code] = {
504
+ "client_id": client_id,
505
+ "redirect_uri": redirect_uri,
506
+ "scope": scope,
507
+ "code_challenge": code_challenge,
508
+ "code_challenge_method": code_challenge_method,
509
+ "expires_at": _now() + self.code_ttl,
510
+ }
511
+ return code
512
+
513
+ def _issue_token(self, *, client_id: str, scope: list[str]) -> Dict[str, Any]:
514
+ access_token = f"mcp_token_{secrets.token_urlsafe(32)}"
515
+ expires_at = _now() + self.token_ttl
516
+ payload = {
517
+ "access_token": access_token,
518
+ "token_type": "Bearer",
519
+ "expires_in": self.token_ttl,
520
+ "expires_at": expires_at,
521
+ "scope": " ".join(scope or self.default_scopes),
522
+ "client_id": client_id,
523
+ }
524
+ self._tokens[access_token] = payload
525
+ return payload
526
+
527
+
528
+ class TokenStore:
529
+ """Abstract token store interface."""
530
+ async def get(self, user_id: str, server_name: str) -> Optional[Dict[str, Any]]: ...
531
+ async def set(self, user_id: str, server_name: str, token: Dict[str, Any]) -> None: ...
532
+ async def delete(self, user_id: str, server_name: str) -> None: ...
533
+
534
+ class InMemoryTokenStore(TokenStore):
535
+ """Simple in-memory token store (not persistent)."""
536
+ def __init__(self):
537
+ self._data = {}
538
+
539
+ async def get(self, user_id, server_name):
540
+ return self._data.get((user_id, server_name))
541
+
542
+ async def set(self, user_id, server_name, token):
543
+ self._data[(user_id, server_name)] = token
544
+
545
+ async def delete(self, user_id, server_name):
546
+ self._data.pop((user_id, server_name), None)
547
+
548
+ class RedisTokenStore(TokenStore):
549
+ """Redis-based token store."""
550
+ def __init__(self, redis):
551
+ self.redis = redis
552
+
553
+ @staticmethod
554
+ def _key(user_id: str, server_name: str) -> str:
555
+ return f"mcp:oauth:{server_name}:{user_id}"
556
+
557
+ async def get(self, user_id, server_name):
558
+ raw = await self.redis.get(self._key(user_id, server_name))
559
+ return json.loads(raw) if raw else None
560
+
561
+ async def set(self, user_id, server_name, token):
562
+ # store with TTL ~ refresh time + cushion if you want, or none
563
+ await self.redis.set(self._key(user_id, server_name), json.dumps(token))
564
+
565
+ async def delete(self, user_id, server_name):
566
+ await self.redis.delete(self._key(user_id, server_name))
567
+
568
+
569
+ # ---- Simple Dynamic Client Registration ----
570
+
571
+ @dataclass
572
+ class RegisteredClient:
573
+ """Represents a registered OAuth client."""
574
+ client_id: str
575
+ client_secret: str
576
+ client_name: str
577
+ redirect_uris: list[str]
578
+ scopes: list[str] = field(default_factory=list)
579
+ created_at: float = field(default_factory=time.time)
580
+
581
+
582
+ class ClientRegistry:
583
+ """
584
+ Minimal in-memory Dynamic Client Registration (RFC 7591) registry.
585
+ Suitable for local development / proxy-style OAuth flows.
586
+ """
587
+ def __init__(self):
588
+ self._clients: Dict[str, RegisteredClient] = {}
589
+
590
+ def register(self, metadata: Dict[str, Any]) -> RegisteredClient:
591
+ if "redirect_uris" not in metadata:
592
+ raise ValueError("redirect_uris is required for client registration")
593
+
594
+ client_id = metadata.get("client_id") or secrets.token_urlsafe(16)
595
+ client_secret = metadata.get("client_secret") or secrets.token_urlsafe(32)
596
+ client_name = metadata.get("client_name") or metadata.get("client_name", "mcp-client")
597
+ redirect_uris = metadata["redirect_uris"]
598
+ scopes = metadata.get("scope", "") or metadata.get("scopes", [])
599
+ if isinstance(scopes, str):
600
+ scopes = scopes.split()
601
+
602
+ client = RegisteredClient(
603
+ client_id=client_id,
604
+ client_secret=client_secret,
605
+ client_name=client_name,
606
+ redirect_uris=redirect_uris,
607
+ scopes=scopes,
608
+ )
609
+ self._clients[client_id] = client
610
+ return client
611
+
612
+ def get(self, client_id: str) -> Optional[RegisteredClient]:
613
+ return self._clients.get(client_id)
614
+
615
+
616
+ class OAuthManager:
617
+ """
618
+ Manages Authorization Code + PKCE flow, token storage, auto refresh,
619
+ and supplies a token string for headers.
620
+ """
621
+ def __init__(
622
+ self,
623
+ *,
624
+ user_id: str,
625
+ server_name: str,
626
+ client_id: str,
627
+ auth_url: str,
628
+ token_url: str,
629
+ scopes: list[str],
630
+ redirect_host: str = "127.0.0.1",
631
+ redirect_port: int = 8765,
632
+ redirect_path: str = "/mcp/oauth/callback",
633
+ token_store: TokenStore,
634
+ client_secret: str | None = None, # if provider requires it
635
+ extra_token_params: dict | None = None,
636
+ http_timeout: float = 15.0,
637
+ ):
638
+ self.user_id = user_id
639
+ self.server_name = server_name
640
+ self.client_id = client_id
641
+ self.client_secret = client_secret
642
+ self.auth_url = auth_url
643
+ self.token_url = token_url
644
+ self.scopes = scopes
645
+ self.redirect_host = redirect_host
646
+ self.redirect_port = redirect_port
647
+ self.redirect_path = redirect_path
648
+ self.redirect_uri = f"http://{redirect_host}:{redirect_port}{redirect_path}"
649
+ self.token_store = token_store
650
+ self.extra_token_params = extra_token_params or {}
651
+ self.http_timeout = http_timeout
652
+
653
+ self._state = secrets.token_urlsafe(24)
654
+ self._verifier = _b64url(os.urandom(32))
655
+ self._challenge = _b64url(hashlib.sha256(self._verifier.encode()).digest())
656
+ self._token: dict | None = None
657
+ self._ready = asyncio.Event()
658
+
659
+ def token_supplier(self) -> Optional[str]:
660
+ # Synchronous hook invoked by the HTTP client layer.
661
+ # We return the current access_token if not expired; otherwise None (caller should await ensure_token()).
662
+ if not self._token:
663
+ return None
664
+ # If near expiry (e.g., within 60s), signal refresh needed
665
+ if self._token.get("expires_at") and self._token["expires_at"] - _now() < 60:
666
+ return None
667
+ return self._token.get("access_token")
668
+
669
+ async def ensure_token(self) -> str:
670
+ """
671
+ Ensures a fresh access token exists:
672
+ - Load from store
673
+ - If expired and refresh_token present -> refresh
674
+ - Else run interactive authorization (PKCE) with local callback
675
+ Returns access_token.
676
+ """
677
+ # 1) Load cached
678
+ cached = await self.token_store.get(self.user_id, self.server_name)
679
+ if cached:
680
+ self._token = cached
681
+
682
+ # 2) If valid, return
683
+ if self._is_token_valid(self._token):
684
+ return self._token["access_token"]
685
+
686
+ # 3) Try refresh
687
+ if self._token and self._token.get("refresh_token"):
688
+ ok = await self._refresh()
689
+ if ok:
690
+ return self._token["access_token"]
691
+
692
+ # 4) Interactive auth
693
+ await self._authorize_interactive()
694
+ return self._token["access_token"]
695
+
696
+ def _is_token_valid(self, tok: Optional[dict]) -> bool:
697
+ if not tok:
698
+ return False
699
+ exp = tok.get("expires_at")
700
+ return bool(tok.get("access_token")) and exp and exp > _now() + 30
701
+
702
+ async def _authorize_interactive(self):
703
+ app = web.Application()
704
+ app.add_routes([web.get(self.redirect_path, self._handle_callback)])
705
+
706
+ runner = web.AppRunner(app)
707
+ await runner.setup()
708
+ site = web.TCPSite(runner, self.redirect_host, self.redirect_port)
709
+ await site.start()
710
+
711
+ # Build auth URL
712
+ params = {
713
+ "response_type": "code",
714
+ "client_id": self.client_id,
715
+ "redirect_uri": self.redirect_uri,
716
+ "scope": " ".join(self.scopes),
717
+ "state": self._state,
718
+ "code_challenge": self._challenge,
719
+ "code_challenge_method": "S256",
720
+ }
721
+ url = f"{self.auth_url}?{urlencode(params)}"
722
+
723
+ # Print URL (or open in browser)
724
+ print(f"[OAuth] Please authenticate here:\n{url}", flush=True, file=sys.stderr)
725
+
726
+ try:
727
+ await asyncio.wait_for(self._ready.wait(), timeout=300) # 5 minutes
728
+ finally:
729
+ await runner.cleanup()
730
+
731
+ if not self._token:
732
+ raise RuntimeError("OAuth failed: no token captured")
733
+
734
+ await self.token_store.set(self.user_id, self.server_name, self._token)
735
+
736
+ async def _handle_callback(self, request: web.Request):
737
+ if request.query.get("state") != self._state:
738
+ return web.Response(status=400, text="Invalid OAuth state")
739
+ code = request.query.get("code")
740
+ if not code:
741
+ return web.Response(status=400, text="Missing code")
742
+
743
+ # Exchange
744
+ async with ClientSession() as sess:
745
+ data = {
746
+ "grant_type": "authorization_code",
747
+ "code": code,
748
+ "redirect_uri": self.redirect_uri,
749
+ "client_id": self.client_id,
750
+ "code_verifier": self._verifier,
751
+ **self.extra_token_params,
752
+ }
753
+ if self.client_secret:
754
+ data["client_secret"] = self.client_secret
755
+
756
+ async with sess.post(self.token_url, data=data, timeout=self.http_timeout) as resp:
757
+ tok = await resp.json()
758
+ if resp.status != 200:
759
+ return web.Response(status=resp.status, text=str(tok))
760
+
761
+ self._token = self._normalize_token(tok)
762
+ self._ready.set()
763
+ return web.Response(text="Authentication complete. You can close this window.")
764
+
765
+ async def _refresh(self) -> bool:
766
+ async with ClientSession() as sess:
767
+ data = {
768
+ "grant_type": "refresh_token",
769
+ "refresh_token": self._token["refresh_token"],
770
+ "client_id": self.client_id,
771
+ **self.extra_token_params,
772
+ }
773
+ if self.client_secret:
774
+ data["client_secret"] = self.client_secret
775
+
776
+ async with sess.post(self.token_url, data=data, timeout=self.http_timeout) as resp:
777
+ tok = await resp.json()
778
+ if resp.status != 200 or "access_token" not in tok:
779
+ return False
780
+
781
+ self._token = self._normalize_token(tok, prev=self._token)
782
+ await self.token_store.set(self.user_id, self.server_name, self._token)
783
+ return True
784
+
785
+ def _normalize_token(self, tok: Dict[str, Any], prev: Dict[str, Any] | None = None) -> Dict[str, Any]:
786
+ # Expect providers to return: access_token, token_type, expires_in, refresh_token?
787
+ expires_in = int(tok.get("expires_in", 3600))
788
+ out = {
789
+ "access_token": tok["access_token"],
790
+ "token_type": tok.get("token_type", "Bearer"),
791
+ "expires_in": expires_in,
792
+ "expires_at": _now() + expires_in,
793
+ "refresh_token": tok.get("refresh_token") or (prev.get("refresh_token") if prev else None),
794
+ "scope": tok.get("scope"),
795
+ "raw": tok,
796
+ }
797
+ return out
798
+
799
+
800
+ class OAuthRoutesMixin:
801
+ """Shared OAuth/DCR utilities for HTTP and SSE transports."""
802
+
803
+ def _init_oauth_support(self):
804
+ self.client_registry = ClientRegistry()
805
+ self._auth_codes: Dict[str, Dict[str, Any]] = {}
806
+
807
+ def _oauth_paths(self) -> Dict[str, str]:
808
+ base = self.base_path.rstrip("/")
809
+ base = base if base else ""
810
+ return {
811
+ "discovery": f"{base}/.well-known/oauth-authorization-server",
812
+ "register": f"{base}/oauth/register",
813
+ "authorize": f"{base}/oauth/authorize",
814
+ "token": f"{base}/oauth/token",
815
+ }
816
+
817
+ def _add_oauth_routes(self, router: web.UrlDispatcher):
818
+ paths = self._oauth_paths()
819
+ router.add_get(paths["discovery"], self._handle_discovery)
820
+ router.add_post(paths["register"], self._handle_registration)
821
+ router.add_get(paths["authorize"], self._handle_authorize)
822
+ router.add_post(paths["token"], self._handle_token)
823
+
824
+ async def _handle_discovery(self, request: web.Request) -> web.Response:
825
+ """RFC 8414: Authorization Server Metadata."""
826
+ base_url = f"{request.scheme}://{request.host}"
827
+ paths = self._oauth_paths()
828
+ metadata = {
829
+ "issuer": base_url,
830
+ "authorization_endpoint": f"{base_url}{paths['authorize']}",
831
+ "token_endpoint": f"{base_url}{paths['token']}",
832
+ "registration_endpoint": f"{base_url}{paths['register']}",
833
+ "response_types_supported": ["code"],
834
+ "grant_types_supported": ["authorization_code"],
835
+ "code_challenge_methods_supported": ["S256"],
836
+ "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
837
+ }
838
+ return web.json_response(metadata)
839
+
840
+ async def _handle_registration(self, request: web.Request) -> web.Response:
841
+ """RFC 7591: Dynamic Client Registration."""
842
+ try:
843
+ data = await request.json()
844
+ client = self.client_registry.register(data)
845
+ self.logger.info(
846
+ "Dynamically registered client: %s (%s)",
847
+ client.client_name,
848
+ client.client_id,
849
+ )
850
+ return web.json_response(
851
+ {
852
+ "client_id": client.client_id,
853
+ "client_secret": client.client_secret,
854
+ "client_id_issued_at": int(client.created_at),
855
+ "client_secret_expires_at": 0,
856
+ "client_name": client.client_name,
857
+ "redirect_uris": client.redirect_uris,
858
+ "scope": " ".join(client.scopes),
859
+ },
860
+ status=201,
861
+ )
862
+ except Exception as e: # pylint: disable=broad-except
863
+ self.logger.error(f"DCR Error: {e}")
864
+ return web.json_response(
865
+ {"error": "invalid_client_metadata", "error_description": str(e)},
866
+ status=400,
867
+ )
868
+
869
+ async def _handle_authorize(self, request: web.Request) -> web.Response:
870
+ """Simplified OAuth 2.0 Authorization Endpoint (auto-approves)."""
871
+ params = request.query
872
+ client_id = params.get("client_id")
873
+ redirect_uri = params.get("redirect_uri")
874
+ state = params.get("state")
875
+ code_challenge = params.get("code_challenge")
876
+ code_challenge_method = params.get("code_challenge_method", "S256")
877
+
878
+ client = self.client_registry.get(client_id) if client_id else None
879
+ if not client:
880
+ return web.Response(text="Invalid Client ID", status=400)
881
+
882
+ if redirect_uri not in client.redirect_uris:
883
+ return web.Response(text="Invalid Redirect URI", status=400)
884
+
885
+ auth_code = f"auth_code_{secrets.token_urlsafe(16)}"
886
+ self._auth_codes[auth_code] = {
887
+ "client_id": client_id,
888
+ "redirect_uri": redirect_uri,
889
+ "scopes": client.scopes,
890
+ "issued_at": time.time(),
891
+ "code_challenge": code_challenge,
892
+ "code_challenge_method": code_challenge_method,
893
+ }
894
+
895
+ target = f"{redirect_uri}?code={auth_code}"
896
+ if state:
897
+ target += f"&state={state}"
898
+
899
+ return web.HTTPFound(target)
900
+
901
+ async def _handle_token(self, request: web.Request) -> web.Response:
902
+ """OAuth 2.0 Token Endpoint (authorization_code)."""
903
+ data = await request.post()
904
+ grant_type = data.get("grant_type")
905
+ code = data.get("code")
906
+ client_id = data.get("client_id")
907
+ client_secret = data.get("client_secret")
908
+
909
+ if grant_type != "authorization_code":
910
+ return web.json_response({"error": "unsupported_grant_type"}, status=400)
911
+
912
+ record = self._auth_codes.pop(code, None)
913
+ if not record:
914
+ return web.json_response({"error": "invalid_grant"}, status=400)
915
+
916
+ if client_id != record["client_id"]:
917
+ return web.json_response({"error": "invalid_client"}, status=400)
918
+
919
+ # Validate client secret if provided in registry
920
+ client = self.client_registry.get(client_id)
921
+ if client and client.client_secret and client_secret and client_secret != client.client_secret:
922
+ return web.json_response({"error": "invalid_client"}, status=401)
923
+
924
+ access_token = f"mcp_token_{secrets.token_urlsafe(32)}"
925
+
926
+ return web.json_response(
927
+ {
928
+ "access_token": access_token,
929
+ "token_type": "Bearer",
930
+ "expires_in": 3600,
931
+ "scope": " ".join(record.get("scopes") or []),
932
+ }
933
+ )