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.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- requirements/requirements-dev.txt +21 -0
parrot/mcp/server.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server Implementation - Expose AI-Parrot Tools via MCP Protocol
|
|
3
|
+
=================================================================
|
|
4
|
+
This creates an MCP server that exposes your existing AbstractTool instances
|
|
5
|
+
as MCP tools that can be consumed by any MCP client.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import asyncio
|
|
9
|
+
import argparse
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
from aiohttp import web
|
|
12
|
+
|
|
13
|
+
# AI-Parrot imports
|
|
14
|
+
from parrot.tools.abstract import AbstractTool
|
|
15
|
+
|
|
16
|
+
# Config
|
|
17
|
+
from parrot.mcp.config import MCPServerConfig
|
|
18
|
+
|
|
19
|
+
# Transports
|
|
20
|
+
from parrot.mcp.transports.stdio import StdioMCPServer
|
|
21
|
+
from parrot.mcp.transports.http import HttpMCPServer
|
|
22
|
+
from parrot.mcp.transports.sse import SseMCPServer
|
|
23
|
+
from parrot.mcp.transports.unix import UnixMCPServer
|
|
24
|
+
from parrot.mcp.transports.quic import QuicMCPServer
|
|
25
|
+
|
|
26
|
+
# Suppress noisy loggers
|
|
27
|
+
logging.getLogger('matplotlib').setLevel(logging.ERROR)
|
|
28
|
+
logging.getLogger('PIL').setLevel(logging.ERROR)
|
|
29
|
+
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
|
30
|
+
logging.getLogger('requests').setLevel(logging.ERROR)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MCPServer:
|
|
34
|
+
"""Main MCP server class that chooses transport."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: MCPServerConfig, parent_app: Optional[web.Application] = None):
|
|
37
|
+
self.config = config
|
|
38
|
+
|
|
39
|
+
if config.transport == "stdio":
|
|
40
|
+
self.server = StdioMCPServer(config)
|
|
41
|
+
elif config.transport == "http":
|
|
42
|
+
self.server = HttpMCPServer(config, parent_app=parent_app)
|
|
43
|
+
elif config.transport == "sse":
|
|
44
|
+
self.server = SseMCPServer(config, parent_app=parent_app)
|
|
45
|
+
elif config.transport == "unix":
|
|
46
|
+
self.server = UnixMCPServer(config)
|
|
47
|
+
elif config.transport == "quic":
|
|
48
|
+
self.server = QuicMCPServer(config)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Unsupported transport: {config.transport}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def register_tool(self, tool: AbstractTool):
|
|
55
|
+
"""Register a tool."""
|
|
56
|
+
self.server.register_tool(tool)
|
|
57
|
+
|
|
58
|
+
def register_tools(self, tools: List[AbstractTool]):
|
|
59
|
+
"""Register multiple tools."""
|
|
60
|
+
self.server.register_tools(tools)
|
|
61
|
+
|
|
62
|
+
async def start(self):
|
|
63
|
+
"""Start the server."""
|
|
64
|
+
await self.server.start()
|
|
65
|
+
|
|
66
|
+
async def stop(self):
|
|
67
|
+
"""Start the server."""
|
|
68
|
+
await self.server.stop()
|
|
69
|
+
|
|
70
|
+
async def __aenter__(self):
|
|
71
|
+
await self.start()
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
75
|
+
await self.stop()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Convenience functions
|
|
79
|
+
|
|
80
|
+
def create_stdio_mcp_server(
|
|
81
|
+
name: str = "ai-parrot-tools",
|
|
82
|
+
tools: Optional[List[AbstractTool]] = None,
|
|
83
|
+
**kwargs
|
|
84
|
+
) -> MCPServer:
|
|
85
|
+
"""Create a stdio MCP server."""
|
|
86
|
+
config = MCPServerConfig(
|
|
87
|
+
name=name,
|
|
88
|
+
transport="stdio",
|
|
89
|
+
**kwargs
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
server = MCPServer(config)
|
|
93
|
+
|
|
94
|
+
if tools:
|
|
95
|
+
server.register_tools(tools)
|
|
96
|
+
|
|
97
|
+
return server
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_http_mcp_server(
|
|
101
|
+
name: str = "ai-parrot-tools",
|
|
102
|
+
host: str = "localhost",
|
|
103
|
+
port: int = 8080,
|
|
104
|
+
tools: Optional[List[AbstractTool]] = None,
|
|
105
|
+
parent_app: Optional[web.Application] = None,
|
|
106
|
+
**kwargs
|
|
107
|
+
) -> MCPServer:
|
|
108
|
+
"""Create an HTTP MCP server."""
|
|
109
|
+
config = MCPServerConfig(
|
|
110
|
+
name=name,
|
|
111
|
+
transport="http",
|
|
112
|
+
host=host,
|
|
113
|
+
port=port,
|
|
114
|
+
**kwargs
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
server = HttpMCPServer(
|
|
118
|
+
config,
|
|
119
|
+
parent_app=parent_app
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if tools:
|
|
123
|
+
server.register_tools(tools)
|
|
124
|
+
|
|
125
|
+
return server
|
|
126
|
+
|
|
127
|
+
def create_sse_mcp_server(
|
|
128
|
+
name: str = "ai-parrot-tools",
|
|
129
|
+
host: str = "localhost",
|
|
130
|
+
port: int = 8080,
|
|
131
|
+
tools: Optional[List[AbstractTool]] = None,
|
|
132
|
+
parent_app: Optional[web.Application] = None,
|
|
133
|
+
**kwargs,
|
|
134
|
+
) -> MCPServer:
|
|
135
|
+
"""Create an SSE MCP server."""
|
|
136
|
+
config = MCPServerConfig(
|
|
137
|
+
name=name,
|
|
138
|
+
transport="sse",
|
|
139
|
+
host=host,
|
|
140
|
+
port=port,
|
|
141
|
+
**kwargs,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
server = MCPServer(config, parent_app=parent_app)
|
|
145
|
+
if tools:
|
|
146
|
+
server.register_tools(tools)
|
|
147
|
+
|
|
148
|
+
return server
|
|
149
|
+
|
|
150
|
+
def create_unix_mcp_server(
|
|
151
|
+
name: str = "ai-parrot-tools",
|
|
152
|
+
socket_path: Optional[str] = None,
|
|
153
|
+
tools: Optional[List[AbstractTool]] = None,
|
|
154
|
+
**kwargs,
|
|
155
|
+
) -> MCPServer:
|
|
156
|
+
"""Create an Unix MCP server."""
|
|
157
|
+
config = MCPServerConfig(
|
|
158
|
+
name=name,
|
|
159
|
+
transport="unix",
|
|
160
|
+
socket_path=socket_path,
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
server = MCPServer(config)
|
|
165
|
+
if tools:
|
|
166
|
+
server.register_tools(tools)
|
|
167
|
+
|
|
168
|
+
return server
|
|
169
|
+
|
|
170
|
+
# CLI support
|
|
171
|
+
async def main():
|
|
172
|
+
"""Main CLI entry point."""
|
|
173
|
+
parser = argparse.ArgumentParser(
|
|
174
|
+
description="AI-Parrot MCP Server"
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument("--transport", choices=["stdio", "http", "sse"], default="stdio",
|
|
177
|
+
help="Transport type")
|
|
178
|
+
parser.add_argument("--host", default="localhost",
|
|
179
|
+
help="Host for HTTP server")
|
|
180
|
+
parser.add_argument("--port", type=int, default=8080,
|
|
181
|
+
help="Port for HTTP server")
|
|
182
|
+
parser.add_argument("--name", default="ai-parrot-tools",
|
|
183
|
+
help="Server name")
|
|
184
|
+
parser.add_argument("--log-level", default="INFO",
|
|
185
|
+
help="Log level")
|
|
186
|
+
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
# Create server config
|
|
190
|
+
config = MCPServerConfig(
|
|
191
|
+
name=args.name,
|
|
192
|
+
transport=args.transport,
|
|
193
|
+
host=args.host,
|
|
194
|
+
port=args.port,
|
|
195
|
+
log_level=args.log_level
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Create server
|
|
199
|
+
server = MCPServer(config)
|
|
200
|
+
|
|
201
|
+
# Register example tools:
|
|
202
|
+
# server.register_tool(YourOpenWeatherTool())
|
|
203
|
+
# server.register_tool(YourDatabaseQueryTool())
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
if args.transport in {"http", "sse"}:
|
|
207
|
+
await server.start()
|
|
208
|
+
print(f"Server running at http://{args.host}:{args.port}")
|
|
209
|
+
print("Press Ctrl+C to stop")
|
|
210
|
+
|
|
211
|
+
# Keep running
|
|
212
|
+
while True:
|
|
213
|
+
await asyncio.sleep(1)
|
|
214
|
+
else:
|
|
215
|
+
# For stdio, just start and let it handle stdin
|
|
216
|
+
await server.start()
|
|
217
|
+
|
|
218
|
+
except KeyboardInterrupt:
|
|
219
|
+
print("\nShutting down...")
|
|
220
|
+
finally:
|
|
221
|
+
await server.stop()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, List, Optional, Any
|
|
5
|
+
from aiohttp import web
|
|
6
|
+
|
|
7
|
+
from parrot.tools.abstract import AbstractTool
|
|
8
|
+
from parrot.mcp.config import MCPServerConfig, AuthMethod
|
|
9
|
+
from parrot.mcp.adapter import MCPToolAdapter
|
|
10
|
+
from parrot.mcp.oauth import OAuthAuthorizationServer, APIKeyStore, ExternalOAuthValidator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MCPServerBase(ABC):
|
|
14
|
+
"""Base class for MCP servers."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: MCPServerConfig):
|
|
17
|
+
self.config = config
|
|
18
|
+
self.tools: Dict[str, MCPToolAdapter] = {}
|
|
19
|
+
self.logger = logging.getLogger(f"MCPServer.{config.name}")
|
|
20
|
+
log_level = getattr(logging, config.log_level.upper(), logging.WARNING)
|
|
21
|
+
self.logger.setLevel(log_level)
|
|
22
|
+
|
|
23
|
+
# Authentication components
|
|
24
|
+
self.oauth_server: Optional[OAuthAuthorizationServer] = None
|
|
25
|
+
self.api_key_store: Optional[APIKeyStore] = None
|
|
26
|
+
self.external_oauth: Optional[ExternalOAuthValidator] = None
|
|
27
|
+
|
|
28
|
+
# Initialize authentication based on method
|
|
29
|
+
self._init_authentication()
|
|
30
|
+
|
|
31
|
+
def _init_authentication(self) -> None:
|
|
32
|
+
"""Initialize authentication based on config.auth_method."""
|
|
33
|
+
auth_method = self.config.auth_method
|
|
34
|
+
|
|
35
|
+
# Backward compatibility: enable_oauth maps to OAUTH2_INTERNAL
|
|
36
|
+
if self.config.enable_oauth and auth_method == AuthMethod.NONE:
|
|
37
|
+
auth_method = AuthMethod.OAUTH2_INTERNAL
|
|
38
|
+
self.config.auth_method = auth_method
|
|
39
|
+
|
|
40
|
+
if auth_method == AuthMethod.API_KEY:
|
|
41
|
+
self.api_key_store = self.config.api_key_store or APIKeyStore()
|
|
42
|
+
self.logger.info("Authentication: API Key enabled")
|
|
43
|
+
|
|
44
|
+
elif auth_method == AuthMethod.OAUTH2_INTERNAL:
|
|
45
|
+
self.oauth_server = OAuthAuthorizationServer(
|
|
46
|
+
default_scopes=self.config.oauth_scopes,
|
|
47
|
+
allow_dynamic_registration=self.config.oauth_allow_dynamic_registration,
|
|
48
|
+
token_ttl=self.config.oauth_token_ttl,
|
|
49
|
+
code_ttl=self.config.oauth_code_ttl,
|
|
50
|
+
)
|
|
51
|
+
self.logger.info("Authentication: OAuth2 (internal) enabled")
|
|
52
|
+
|
|
53
|
+
elif auth_method == AuthMethod.OAUTH2_EXTERNAL:
|
|
54
|
+
if not self.config.oauth2_introspection_endpoint:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"oauth2_introspection_endpoint required for OAUTH2_EXTERNAL"
|
|
57
|
+
)
|
|
58
|
+
self.external_oauth = ExternalOAuthValidator(
|
|
59
|
+
introspection_endpoint=self.config.oauth2_introspection_endpoint,
|
|
60
|
+
client_id=self.config.oauth2_client_id or "",
|
|
61
|
+
client_secret=self.config.oauth2_client_secret or "",
|
|
62
|
+
resource_server_url=self.config.oauth2_resource_server_url,
|
|
63
|
+
)
|
|
64
|
+
self.logger.info(
|
|
65
|
+
f"Authentication: OAuth2 (external) enabled - {self.config.oauth2_issuer_url}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
elif auth_method == AuthMethod.BEARER:
|
|
69
|
+
self.logger.info("Authentication: Bearer (navigator-auth) enabled")
|
|
70
|
+
|
|
71
|
+
else:
|
|
72
|
+
self.logger.debug("Authentication: None (open access)")
|
|
73
|
+
|
|
74
|
+
def register_tool(self, tool: AbstractTool):
|
|
75
|
+
"""Register an AI-Parrot tool with the MCP server."""
|
|
76
|
+
tool_name = tool.name
|
|
77
|
+
|
|
78
|
+
# Apply filtering
|
|
79
|
+
if self.config.allowed_tools and tool_name not in self.config.allowed_tools:
|
|
80
|
+
self.logger.info(f"Skipping tool {tool_name} (not in allowed_tools)")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if self.config.blocked_tools and tool_name in self.config.blocked_tools:
|
|
84
|
+
self.logger.info(f"Skipping tool {tool_name} (in blocked_tools)")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
adapter = MCPToolAdapter(tool)
|
|
88
|
+
self.tools[tool_name] = adapter
|
|
89
|
+
self.logger.info(f"Registered tool: {tool_name}")
|
|
90
|
+
|
|
91
|
+
def register_tools(self, tools: List[AbstractTool]):
|
|
92
|
+
"""Register multiple tools."""
|
|
93
|
+
for tool in tools:
|
|
94
|
+
self.register_tool(tool)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _authenticate_request(self, request: web.Request) -> Optional[web.Response]:
|
|
98
|
+
"""
|
|
99
|
+
Authenticate request based on configured auth method.
|
|
100
|
+
|
|
101
|
+
Returns None if authenticated, or a web.Response with error if not.
|
|
102
|
+
"""
|
|
103
|
+
auth_method = self.config.auth_method
|
|
104
|
+
|
|
105
|
+
if auth_method == AuthMethod.NONE:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
elif auth_method == AuthMethod.API_KEY:
|
|
109
|
+
return await self._authenticate_api_key(request)
|
|
110
|
+
|
|
111
|
+
elif auth_method == AuthMethod.OAUTH2_INTERNAL:
|
|
112
|
+
return self._authenticate_oauth_internal(request)
|
|
113
|
+
|
|
114
|
+
elif auth_method == AuthMethod.OAUTH2_EXTERNAL:
|
|
115
|
+
return await self._authenticate_oauth_external(request)
|
|
116
|
+
|
|
117
|
+
elif auth_method == AuthMethod.BEARER:
|
|
118
|
+
return await self._authenticate_bearer(request)
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
async def _authenticate_api_key(self, request: web.Request) -> Optional[web.Response]:
|
|
123
|
+
"""Validate API key from header."""
|
|
124
|
+
api_key = request.headers.get(self.config.api_key_header)
|
|
125
|
+
if not api_key:
|
|
126
|
+
return self._unauthorized_response(
|
|
127
|
+
"API key required",
|
|
128
|
+
f'X-API-Key realm="mcp"'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
record = self.api_key_store.validate_key(api_key)
|
|
132
|
+
if not record:
|
|
133
|
+
return self._unauthorized_response("Invalid or expired API key")
|
|
134
|
+
|
|
135
|
+
# Log session start
|
|
136
|
+
self.api_key_store.log_session_start(api_key, record.user_id, time.time())
|
|
137
|
+
self.logger.debug(f"API key authenticated for user: {record.user_id}")
|
|
138
|
+
|
|
139
|
+
# Store user info in request for downstream use
|
|
140
|
+
request["mcp_user"] = {"user_id": record.user_id, "scopes": record.scopes}
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def _authenticate_oauth_internal(self, request: web.Request) -> Optional[web.Response]:
|
|
144
|
+
"""Validate OAuth access token from internal OAuth server."""
|
|
145
|
+
if not self.oauth_server:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
token = self.oauth_server.bearer_token_from_header(
|
|
149
|
+
request.headers.get("Authorization")
|
|
150
|
+
)
|
|
151
|
+
if not self.oauth_server.is_token_valid(token):
|
|
152
|
+
return self._unauthorized_response("Valid Bearer token is required")
|
|
153
|
+
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
async def _authenticate_oauth_external(self, request: web.Request) -> Optional[web.Response]:
|
|
157
|
+
"""Validate OAuth access token via external introspection."""
|
|
158
|
+
if not self.external_oauth:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
token = self._extract_bearer_token(request.headers.get("Authorization"))
|
|
162
|
+
if not token:
|
|
163
|
+
return self._unauthorized_response("Bearer token required")
|
|
164
|
+
|
|
165
|
+
token_info = await self.external_oauth.validate_token(token)
|
|
166
|
+
if not token_info:
|
|
167
|
+
return self._unauthorized_response("Invalid or expired token")
|
|
168
|
+
|
|
169
|
+
# Store token info in request
|
|
170
|
+
request["mcp_user"] = {
|
|
171
|
+
"user_id": token_info.get("sub") or token_info.get("client_id"),
|
|
172
|
+
"scopes": token_info.get("scope", "").split() if token_info.get("scope") else [],
|
|
173
|
+
"token_info": token_info,
|
|
174
|
+
}
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
async def _authenticate_bearer(self, request: web.Request) -> Optional[web.Response]:
|
|
178
|
+
"""Validate bearer token via navigator-auth."""
|
|
179
|
+
auth = request.app.get('auth')
|
|
180
|
+
if not auth:
|
|
181
|
+
self.logger.warning("navigator-auth not configured in app['auth']")
|
|
182
|
+
# Fall through if auth not configured (development mode)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
userdata = await auth.get_session(request)
|
|
187
|
+
if not userdata:
|
|
188
|
+
return web.json_response(
|
|
189
|
+
{"error": "unauthorized", "error_description": "Session required"},
|
|
190
|
+
status=401,
|
|
191
|
+
headers={"WWW-Authenticate": 'Bearer realm="mcp"'}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Store user info in request
|
|
195
|
+
request["mcp_user"] = userdata
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self.logger.error(f"navigator-auth error: {e}")
|
|
200
|
+
return web.json_response(
|
|
201
|
+
{"error": "unauthorized", "error_description": "Authentication failed"},
|
|
202
|
+
status=401,
|
|
203
|
+
headers={"WWW-Authenticate": 'Bearer realm="mcp"'}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _extract_bearer_token(self, auth_header: Optional[str]) -> Optional[str]:
|
|
207
|
+
"""Extract bearer token from Authorization header."""
|
|
208
|
+
if not auth_header:
|
|
209
|
+
return None
|
|
210
|
+
if not auth_header.lower().startswith("bearer "):
|
|
211
|
+
return None
|
|
212
|
+
return auth_header.split(" ", 1)[1].strip()
|
|
213
|
+
|
|
214
|
+
def _unauthorized_response(
|
|
215
|
+
self,
|
|
216
|
+
message: str,
|
|
217
|
+
www_authenticate: str = 'Bearer realm="mcp"'
|
|
218
|
+
) -> web.Response:
|
|
219
|
+
"""Create a 401 unauthorized response."""
|
|
220
|
+
return web.json_response(
|
|
221
|
+
{"error": "unauthorized", "error_description": message},
|
|
222
|
+
status=401,
|
|
223
|
+
headers={"WWW-Authenticate": www_authenticate}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
228
|
+
"""Handle MCP initialize request."""
|
|
229
|
+
self.logger.info("Initializing MCP server...")
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"protocolVersion": "2024-11-05",
|
|
233
|
+
"capabilities": {
|
|
234
|
+
"tools": {
|
|
235
|
+
"listChanged": False
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"serverInfo": {
|
|
239
|
+
"name": self.config.name,
|
|
240
|
+
"version": self.config.version,
|
|
241
|
+
"description": self.config.description
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async def handle_tools_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
246
|
+
"""Handle tools/list request."""
|
|
247
|
+
self.logger.info(f"Listing {len(self.tools)} available tools")
|
|
248
|
+
|
|
249
|
+
tools = []
|
|
250
|
+
tools.extend(
|
|
251
|
+
adapter.to_mcp_tool_definition() for adapter in self.tools.values()
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return {"tools": tools}
|
|
255
|
+
|
|
256
|
+
async def handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
257
|
+
"""Handle tools/call request."""
|
|
258
|
+
tool_name = params.get("name")
|
|
259
|
+
arguments = params.get("arguments", {})
|
|
260
|
+
|
|
261
|
+
self.logger.info(f"Calling tool: {tool_name} with args: {arguments}")
|
|
262
|
+
|
|
263
|
+
if tool_name not in self.tools:
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"Tool not found: {tool_name}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
adapter = self.tools[tool_name]
|
|
269
|
+
return await adapter.execute(arguments)
|
|
270
|
+
|
|
271
|
+
@abstractmethod
|
|
272
|
+
async def start(self):
|
|
273
|
+
"""Start the MCP server."""
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
@abstractmethod
|
|
277
|
+
async def stop(self):
|
|
278
|
+
"""Stop the MCP server."""
|
|
279
|
+
pass
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
# from .proto import mcp_pb2, mcp_pb2_grpc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class GrpcMCPConfig:
|
|
10
|
+
"""Configuration for gRPC MCP transport."""
|
|
11
|
+
host: str = "localhost"
|
|
12
|
+
port: int = 50051
|
|
13
|
+
use_tls: bool = True
|
|
14
|
+
cert_path: Optional[str] = None
|
|
15
|
+
# For protobuf message format instead of JSON-RPC
|
|
16
|
+
use_protobuf_messages: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GrpcMCPSession:
|
|
20
|
+
"""MCP session for gRPC transport with optional protobuf messages."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: 'MCPServerConfig', logger):
|
|
23
|
+
self.config = config
|
|
24
|
+
self.logger = logger
|
|
25
|
+
self._request_id = 0
|
|
26
|
+
self._channel: Optional[grpc.aio.Channel] = None
|
|
27
|
+
self._stub = None
|
|
28
|
+
self._initialized = False
|
|
29
|
+
self._response_queue: asyncio.Queue = asyncio.Queue()
|
|
30
|
+
self._stream_task: Optional[asyncio.Task] = None
|
|
31
|
+
|
|
32
|
+
async def connect(self):
|
|
33
|
+
"""Connect to MCP server via gRPC."""
|
|
34
|
+
try:
|
|
35
|
+
grpc_config = self.config.grpc_config or GrpcMCPConfig()
|
|
36
|
+
|
|
37
|
+
# Create channel
|
|
38
|
+
target = f"{grpc_config.host}:{grpc_config.port}"
|
|
39
|
+
|
|
40
|
+
if grpc_config.use_tls:
|
|
41
|
+
if grpc_config.cert_path:
|
|
42
|
+
with open(grpc_config.cert_path, 'rb') as f:
|
|
43
|
+
creds = grpc.ssl_channel_credentials(f.read())
|
|
44
|
+
else:
|
|
45
|
+
creds = grpc.ssl_channel_credentials()
|
|
46
|
+
self._channel = grpc.aio.secure_channel(target, creds)
|
|
47
|
+
else:
|
|
48
|
+
self._channel = grpc.aio.insecure_channel(target)
|
|
49
|
+
|
|
50
|
+
# Create stub
|
|
51
|
+
self._stub = mcp_pb2_grpc.MCPServiceStub(self._channel)
|
|
52
|
+
|
|
53
|
+
# Start bidirectional stream
|
|
54
|
+
self._stream = self._stub.BiDirectionalStream()
|
|
55
|
+
self._stream_task = asyncio.create_task(self._read_responses())
|
|
56
|
+
|
|
57
|
+
# Initialize MCP session
|
|
58
|
+
await self._initialize_session()
|
|
59
|
+
self._initialized = True
|
|
60
|
+
|
|
61
|
+
self.logger.info(f"gRPC connection established to {self.config.name}")
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
await self.disconnect()
|
|
65
|
+
raise MCPConnectionError(f"gRPC connection failed: {e}") from e
|
|
66
|
+
|
|
67
|
+
async def _read_responses(self):
|
|
68
|
+
"""Background task to read responses from gRPC stream."""
|
|
69
|
+
try:
|
|
70
|
+
async for response in self._stream:
|
|
71
|
+
# Convert protobuf to dict if needed
|
|
72
|
+
if hasattr(response, 'json_rpc_message'):
|
|
73
|
+
msg = json.loads(response.json_rpc_message)
|
|
74
|
+
else:
|
|
75
|
+
msg = self._protobuf_to_dict(response)
|
|
76
|
+
|
|
77
|
+
await self._response_queue.put(msg)
|
|
78
|
+
except grpc.aio.AioRpcError as e:
|
|
79
|
+
self.logger.error(f"gRPC stream error: {e}")
|
|
80
|
+
except asyncio.CancelledError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
async def send_request(self, method: str, params: Dict = None) -> Dict[str, Any]:
|
|
84
|
+
"""Send JSON-RPC request over gRPC."""
|
|
85
|
+
self._request_id += 1
|
|
86
|
+
|
|
87
|
+
request = {
|
|
88
|
+
"jsonrpc": "2.0",
|
|
89
|
+
"id": self._request_id,
|
|
90
|
+
"method": method,
|
|
91
|
+
"params": params or {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Option 1: Send JSON-RPC as string in protobuf wrapper
|
|
95
|
+
grpc_request = mcp_pb2.MCPRequest(
|
|
96
|
+
json_rpc_message=json.dumps(request)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Option 2: Use native protobuf messages (more efficient)
|
|
100
|
+
# grpc_request = self._dict_to_protobuf(request)
|
|
101
|
+
|
|
102
|
+
await self._stream.write(grpc_request)
|
|
103
|
+
|
|
104
|
+
# Wait for response with matching ID
|
|
105
|
+
while True:
|
|
106
|
+
response = await asyncio.wait_for(
|
|
107
|
+
self._response_queue.get(),
|
|
108
|
+
timeout=self.config.timeout
|
|
109
|
+
)
|
|
110
|
+
if response.get("id") == self._request_id:
|
|
111
|
+
return response
|
|
112
|
+
# Put back if not matching (notifications, etc.)
|
|
113
|
+
await self._response_queue.put(response)
|
|
114
|
+
|
|
115
|
+
async def _initialize_session(self):
|
|
116
|
+
"""Initialize MCP session."""
|
|
117
|
+
response = await self.send_request("initialize", {
|
|
118
|
+
"protocolVersion": "2024-11-05",
|
|
119
|
+
"capabilities": {},
|
|
120
|
+
"clientInfo": {
|
|
121
|
+
"name": "ai-parrot",
|
|
122
|
+
"version": "1.0.0"
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if "error" in response:
|
|
127
|
+
raise MCPConnectionError(f"Initialize failed: {response['error']}")
|
|
128
|
+
|
|
129
|
+
# Send initialized notification
|
|
130
|
+
await self._stream.write(mcp_pb2.MCPRequest(
|
|
131
|
+
json_rpc_message=json.dumps({
|
|
132
|
+
"jsonrpc": "2.0",
|
|
133
|
+
"method": "notifications/initialized"
|
|
134
|
+
})
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
async def list_tools(self) -> list:
|
|
138
|
+
"""List available tools."""
|
|
139
|
+
response = await self.send_request("tools/list")
|
|
140
|
+
return response.get("result", {}).get("tools", [])
|
|
141
|
+
|
|
142
|
+
async def call_tool(self, name: str, arguments: Dict) -> Dict:
|
|
143
|
+
"""Call a tool."""
|
|
144
|
+
response = await self.send_request("tools/call", {
|
|
145
|
+
"name": name,
|
|
146
|
+
"arguments": arguments
|
|
147
|
+
})
|
|
148
|
+
return response.get("result", {})
|
|
149
|
+
|
|
150
|
+
async def disconnect(self):
|
|
151
|
+
"""Disconnect from gRPC server."""
|
|
152
|
+
self._initialized = False
|
|
153
|
+
|
|
154
|
+
if self._stream_task and not self._stream_task.done():
|
|
155
|
+
self._stream_task.cancel()
|
|
156
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
157
|
+
await self._stream_task
|
|
158
|
+
|
|
159
|
+
if self._channel:
|
|
160
|
+
await self._channel.close()
|
|
161
|
+
self._channel = None
|
|
162
|
+
|
|
163
|
+
self.logger.info("gRPC disconnected")
|