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
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QUIC/HTTP3 MCP Server Implementation
|
|
3
|
+
====================================
|
|
4
|
+
|
|
5
|
+
High-performance MCP server using QUIC/HTTP3 with WebTransport support.
|
|
6
|
+
Provides ultra-low latency for distributed MCP deployments.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- 0-RTT connection establishment
|
|
10
|
+
- Multiplexed streams without head-of-line blocking
|
|
11
|
+
- Binary serialization (MessagePack) for efficiency
|
|
12
|
+
- Unreliable datagrams for telemetry
|
|
13
|
+
- Connection migration for mobile agents
|
|
14
|
+
|
|
15
|
+
Requires:
|
|
16
|
+
pip install aioquic msgpack --break-system-packages
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
server = QuicMCPServer(config)
|
|
20
|
+
server.register_tool(MyTool())
|
|
21
|
+
await server.start()
|
|
22
|
+
|
|
23
|
+
# For development, create the certificate:
|
|
24
|
+
# openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import ssl
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from enum import Enum
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
# Core QUIC library
|
|
38
|
+
from aioquic.asyncio import (
|
|
39
|
+
QuicConnectionProtocol,
|
|
40
|
+
connect as quic_connect,
|
|
41
|
+
serve as quic_serve
|
|
42
|
+
)
|
|
43
|
+
from aioquic.asyncio.server import QuicServer
|
|
44
|
+
from aioquic.h3.connection import H3_ALPN, H3Connection
|
|
45
|
+
from aioquic.h3.events import (
|
|
46
|
+
DataReceived,
|
|
47
|
+
H3Event,
|
|
48
|
+
HeadersReceived,
|
|
49
|
+
WebTransportStreamDataReceived,
|
|
50
|
+
)
|
|
51
|
+
from aioquic.quic.configuration import QuicConfiguration
|
|
52
|
+
from aioquic.quic.events import (
|
|
53
|
+
ConnectionTerminated,
|
|
54
|
+
DatagramFrameReceived,
|
|
55
|
+
ProtocolNegotiated,
|
|
56
|
+
QuicEvent,
|
|
57
|
+
StreamDataReceived,
|
|
58
|
+
)
|
|
59
|
+
from aioquic.tls import SessionTicket
|
|
60
|
+
from .base import MCPServerBase
|
|
61
|
+
from ..config import MCPServerConfig
|
|
62
|
+
|
|
63
|
+
# Optional: faster serialization
|
|
64
|
+
try:
|
|
65
|
+
import msgpack
|
|
66
|
+
HAS_MSGPACK = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
HAS_MSGPACK = False
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from google.protobuf import json_format
|
|
72
|
+
HAS_PROTOBUF = True
|
|
73
|
+
except ImportError:
|
|
74
|
+
HAS_PROTOBUF = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SerializationFormat(Enum):
|
|
78
|
+
"""Supported serialization formats for MCP messages."""
|
|
79
|
+
JSON = "json" # Standard, compatible
|
|
80
|
+
MSGPACK = "msgpack" # ~2-3x faster, smaller
|
|
81
|
+
PROTOBUF = "protobuf" # Fastest, smallest, needs schema
|
|
82
|
+
|
|
83
|
+
class MCPConnectionError(Exception):
|
|
84
|
+
"""MCP connection error."""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class QuicMCPConfig:
|
|
89
|
+
"""Unified QUIC configuration."""
|
|
90
|
+
# Connection
|
|
91
|
+
host: str = "localhost"
|
|
92
|
+
port: int = 4433
|
|
93
|
+
|
|
94
|
+
# TLS (required for QUIC)
|
|
95
|
+
cert_path: str = "cert.pem"
|
|
96
|
+
key_path: str = "key.pem"
|
|
97
|
+
ca_cert_path: Optional[str] = None
|
|
98
|
+
verify_mode: ssl.VerifyMode = ssl.CERT_REQUIRED
|
|
99
|
+
insecure: bool = False # For development only
|
|
100
|
+
|
|
101
|
+
# Performance
|
|
102
|
+
max_datagram_size: int = 65536
|
|
103
|
+
idle_timeout: float = 60.0
|
|
104
|
+
max_streams_bidi: int = 128
|
|
105
|
+
max_streams_uni: int = 128
|
|
106
|
+
|
|
107
|
+
# Serialization
|
|
108
|
+
serialization: SerializationFormat = SerializationFormat.MSGPACK
|
|
109
|
+
|
|
110
|
+
# 0-RTT
|
|
111
|
+
enable_0rtt: bool = True
|
|
112
|
+
session_ticket_path: Optional[str] = None
|
|
113
|
+
|
|
114
|
+
# WebTransport
|
|
115
|
+
enable_webtransport: bool = True
|
|
116
|
+
webtransport_path: str = "/mcp"
|
|
117
|
+
|
|
118
|
+
# Advanced
|
|
119
|
+
congestion_control: str = "cubic"
|
|
120
|
+
quic_log_dir: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MCPSerializer:
|
|
124
|
+
"""Handles serialization/deserialization of MCP messages."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, format: SerializationFormat = SerializationFormat.MSGPACK):
|
|
127
|
+
self.format = format
|
|
128
|
+
|
|
129
|
+
if format == SerializationFormat.MSGPACK and not HAS_MSGPACK:
|
|
130
|
+
logging.warning("msgpack not available, falling back to JSON")
|
|
131
|
+
self.format = SerializationFormat.JSON
|
|
132
|
+
|
|
133
|
+
def serialize(self, message: Dict[str, Any]) -> bytes:
|
|
134
|
+
"""Serialize MCP message to bytes."""
|
|
135
|
+
if self.format == SerializationFormat.MSGPACK:
|
|
136
|
+
return msgpack.packb(message, use_bin_type=True)
|
|
137
|
+
elif self.format == SerializationFormat.PROTOBUF:
|
|
138
|
+
# Would need generated protobuf classes
|
|
139
|
+
raise NotImplementedError("Protobuf serialization requires schema")
|
|
140
|
+
else:
|
|
141
|
+
return json.dumps(message).encode('utf-8')
|
|
142
|
+
|
|
143
|
+
def deserialize(self, data: bytes) -> Dict[str, Any]:
|
|
144
|
+
"""Deserialize bytes to MCP message."""
|
|
145
|
+
if self.format == SerializationFormat.MSGPACK:
|
|
146
|
+
return msgpack.unpackb(data, raw=False)
|
|
147
|
+
elif self.format == SerializationFormat.PROTOBUF:
|
|
148
|
+
# Would need generated protobuf classes
|
|
149
|
+
raise NotImplementedError("Protobuf serialization requires schema")
|
|
150
|
+
else:
|
|
151
|
+
return json.loads(data.decode('utf-8'))
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def content_type(self) -> str:
|
|
155
|
+
"""MIME type for the serialization format."""
|
|
156
|
+
return {
|
|
157
|
+
SerializationFormat.JSON: "application/json",
|
|
158
|
+
SerializationFormat.MSGPACK: "application/msgpack",
|
|
159
|
+
SerializationFormat.PROTOBUF: "application/x-protobuf",
|
|
160
|
+
}[self.format]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class QuicMCPClientProtocol(QuicConnectionProtocol):
|
|
164
|
+
"""QUIC protocol handler for MCP client connections."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, *args, **kwargs):
|
|
167
|
+
super().__init__(*args, **kwargs)
|
|
168
|
+
self._http: Optional[H3Connection] = None
|
|
169
|
+
self._request_id = 0
|
|
170
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
171
|
+
self._stream_queues: Dict[int, asyncio.Queue] = {}
|
|
172
|
+
self._session_id: Optional[int] = None
|
|
173
|
+
self._initialized = False
|
|
174
|
+
self.logger = logging.getLogger("QuicMCPClient")
|
|
175
|
+
self.serializer: Optional[MCPSerializer] = None
|
|
176
|
+
|
|
177
|
+
def quic_event_received(self, event: QuicEvent):
|
|
178
|
+
"""Handle QUIC-level events."""
|
|
179
|
+
if isinstance(event, ConnectionTerminated):
|
|
180
|
+
self.logger.warning(f"Connection terminated: {event.reason_phrase}")
|
|
181
|
+
# Cancel all pending requests
|
|
182
|
+
for future in self._pending_requests.values():
|
|
183
|
+
if not future.done():
|
|
184
|
+
future.set_exception(
|
|
185
|
+
ConnectionError(f"Connection lost: {event.reason_phrase}")
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if isinstance(event, DatagramFrameReceived):
|
|
190
|
+
# Handle unreliable datagrams (telemetry, heartbeats)
|
|
191
|
+
self._handle_datagram(event.data)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Pass to HTTP/3 layer
|
|
195
|
+
if self._http is not None:
|
|
196
|
+
for h3_event in self._http.handle_event(event):
|
|
197
|
+
self._handle_h3_event(h3_event)
|
|
198
|
+
|
|
199
|
+
def _handle_h3_event(self, event: H3Event):
|
|
200
|
+
"""Handle HTTP/3 level events."""
|
|
201
|
+
if isinstance(event, HeadersReceived):
|
|
202
|
+
# Response headers received
|
|
203
|
+
stream_id = event.stream_id
|
|
204
|
+
if stream_id not in self._stream_queues:
|
|
205
|
+
self._stream_queues[stream_id] = asyncio.Queue()
|
|
206
|
+
|
|
207
|
+
elif isinstance(event, DataReceived):
|
|
208
|
+
# Response data received
|
|
209
|
+
stream_id = event.stream_id
|
|
210
|
+
if stream_id in self._stream_queues:
|
|
211
|
+
asyncio.get_event_loop().call_soon(
|
|
212
|
+
self._stream_queues[stream_id].put_nowait,
|
|
213
|
+
event.data
|
|
214
|
+
)
|
|
215
|
+
if event.stream_ended:
|
|
216
|
+
asyncio.get_event_loop().call_soon(
|
|
217
|
+
self._stream_queues[stream_id].put_nowait,
|
|
218
|
+
None # Signal end of stream
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
elif isinstance(event, WebTransportStreamDataReceived):
|
|
222
|
+
# WebTransport bidirectional stream data
|
|
223
|
+
self._handle_webtransport_data(event)
|
|
224
|
+
|
|
225
|
+
def _handle_webtransport_data(self, event: WebTransportStreamDataReceived):
|
|
226
|
+
"""Handle WebTransport stream data."""
|
|
227
|
+
try:
|
|
228
|
+
message = self.serializer.deserialize(event.data)
|
|
229
|
+
|
|
230
|
+
# Match response to request
|
|
231
|
+
msg_id = message.get("id")
|
|
232
|
+
if msg_id is not None and msg_id in self._pending_requests:
|
|
233
|
+
future = self._pending_requests.pop(msg_id)
|
|
234
|
+
if not future.done():
|
|
235
|
+
future.set_result(message)
|
|
236
|
+
else:
|
|
237
|
+
# Server-initiated message (notification)
|
|
238
|
+
self._handle_notification(message)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.logger.error(f"Failed to handle WebTransport data: {e}")
|
|
242
|
+
|
|
243
|
+
def _handle_notification(self, message: Dict[str, Any]):
|
|
244
|
+
"""Handle server-initiated notifications."""
|
|
245
|
+
method = message.get("method", "")
|
|
246
|
+
self.logger.debug(f"Received notification: {method}")
|
|
247
|
+
# Could emit to event handlers here
|
|
248
|
+
|
|
249
|
+
def _handle_datagram(self, data: bytes):
|
|
250
|
+
"""Handle unreliable datagram (telemetry, etc.)."""
|
|
251
|
+
try:
|
|
252
|
+
# Datagrams use a simple format: 1 byte type + payload
|
|
253
|
+
msg_type = data[0]
|
|
254
|
+
payload = data[1:]
|
|
255
|
+
|
|
256
|
+
if msg_type == 0x01: # Heartbeat
|
|
257
|
+
self.logger.debug("Received heartbeat")
|
|
258
|
+
elif msg_type == 0x02: # Telemetry
|
|
259
|
+
self.logger.debug(f"Received telemetry: {len(payload)} bytes")
|
|
260
|
+
# Add more types as needed
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
self.logger.debug(f"Failed to handle datagram: {e}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class QuicMCPServerProtocol(QuicConnectionProtocol):
|
|
267
|
+
"""
|
|
268
|
+
QUIC protocol handler for MCP server connections.
|
|
269
|
+
|
|
270
|
+
Handles:
|
|
271
|
+
- HTTP/3 requests
|
|
272
|
+
- WebTransport sessions
|
|
273
|
+
- Unreliable datagrams for telemetry
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(self, *args, mcp_handler: 'QuicMCPServer', **kwargs):
|
|
277
|
+
super().__init__(*args, **kwargs)
|
|
278
|
+
self.mcp_handler = mcp_handler
|
|
279
|
+
self._http: Optional[H3Connection] = None
|
|
280
|
+
self.serializer = mcp_handler.serializer
|
|
281
|
+
self.logger = logging.getLogger("QuicMCPServerProtocol")
|
|
282
|
+
|
|
283
|
+
# WebTransport session tracking
|
|
284
|
+
self._webtransport_sessions: Dict[int, Dict[str, Any]] = {}
|
|
285
|
+
|
|
286
|
+
# Buffer for incomplete messages
|
|
287
|
+
self._stream_buffers: Dict[int, bytes] = {}
|
|
288
|
+
|
|
289
|
+
def quic_event_received(self, event: QuicEvent) -> None:
|
|
290
|
+
"""Handle QUIC-level events."""
|
|
291
|
+
|
|
292
|
+
if isinstance(event, ProtocolNegotiated):
|
|
293
|
+
# Protocol negotiated, setup HTTP/3
|
|
294
|
+
if event.alpn_protocol in H3_ALPN:
|
|
295
|
+
self._http = H3Connection(
|
|
296
|
+
self._quic,
|
|
297
|
+
enable_webtransport=self.mcp_handler.quic_config.enable_webtransport
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if isinstance(event, ConnectionTerminated):
|
|
302
|
+
self.logger.info(
|
|
303
|
+
f"Connection terminated: {event.error_code} - {event.reason_phrase}"
|
|
304
|
+
)
|
|
305
|
+
# Cleanup WebTransport sessions
|
|
306
|
+
self._webtransport_sessions.clear()
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if isinstance(event, DatagramFrameReceived):
|
|
310
|
+
# Handle unreliable datagrams (telemetry, heartbeats)
|
|
311
|
+
asyncio.create_task(self._handle_datagram(event.data))
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# Pass to HTTP/3 layer
|
|
315
|
+
if self._http is not None:
|
|
316
|
+
for h3_event in self._http.handle_event(event):
|
|
317
|
+
asyncio.create_task(self._handle_h3_event(h3_event))
|
|
318
|
+
|
|
319
|
+
async def _handle_h3_event(self, event: H3Event) -> None:
|
|
320
|
+
"""Handle HTTP/3 level events."""
|
|
321
|
+
|
|
322
|
+
if isinstance(event, HeadersReceived):
|
|
323
|
+
await self._handle_headers(event)
|
|
324
|
+
|
|
325
|
+
elif isinstance(event, DataReceived):
|
|
326
|
+
await self._handle_data(event)
|
|
327
|
+
|
|
328
|
+
elif isinstance(event, WebTransportStreamDataReceived):
|
|
329
|
+
await self._handle_webtransport_data(event)
|
|
330
|
+
|
|
331
|
+
async def _handle_headers(self, event: HeadersReceived) -> None:
|
|
332
|
+
"""Handle incoming request headers."""
|
|
333
|
+
headers = dict(event.headers)
|
|
334
|
+
method = headers.get(b":method", b"").decode()
|
|
335
|
+
path = headers.get(b":path", b"/").decode()
|
|
336
|
+
|
|
337
|
+
self.logger.debug(f"Headers received: {method} {path}")
|
|
338
|
+
|
|
339
|
+
# Check for WebTransport CONNECT
|
|
340
|
+
if method == "CONNECT" and headers.get(b":protocol") == b"webtransport":
|
|
341
|
+
await self._accept_webtransport(event.stream_id, path)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
# Regular HTTP/3 POST for MCP
|
|
345
|
+
if method == "POST" and path == self.mcp_handler.quic_config.webtransport_path:
|
|
346
|
+
# Data will come in DataReceived event
|
|
347
|
+
self._stream_buffers[event.stream_id] = b""
|
|
348
|
+
|
|
349
|
+
async def _accept_webtransport(self, stream_id: int, path: str) -> None:
|
|
350
|
+
"""Accept a WebTransport session."""
|
|
351
|
+
self._webtransport_sessions[stream_id] = {
|
|
352
|
+
"path": path,
|
|
353
|
+
"created_at": asyncio.get_event_loop().time(),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Send success response
|
|
357
|
+
self._http.send_headers(
|
|
358
|
+
stream_id=stream_id,
|
|
359
|
+
headers=[
|
|
360
|
+
(b":status", b"200"),
|
|
361
|
+
(b"sec-webtransport-http3-draft", b"draft02"),
|
|
362
|
+
],
|
|
363
|
+
)
|
|
364
|
+
self.transmit()
|
|
365
|
+
|
|
366
|
+
self.logger.info(f"WebTransport session accepted: {stream_id}")
|
|
367
|
+
|
|
368
|
+
async def _handle_data(self, event: DataReceived) -> None:
|
|
369
|
+
"""Handle incoming HTTP/3 data."""
|
|
370
|
+
stream_id = event.stream_id
|
|
371
|
+
|
|
372
|
+
# Accumulate data
|
|
373
|
+
if stream_id in self._stream_buffers:
|
|
374
|
+
self._stream_buffers[stream_id] += event.data
|
|
375
|
+
else:
|
|
376
|
+
self._stream_buffers[stream_id] = event.data
|
|
377
|
+
|
|
378
|
+
# Process if stream ended
|
|
379
|
+
if event.stream_ended:
|
|
380
|
+
data = self._stream_buffers.pop(stream_id, b"")
|
|
381
|
+
if data:
|
|
382
|
+
await self._process_mcp_request(stream_id, data)
|
|
383
|
+
|
|
384
|
+
async def _handle_webtransport_data(self, event: WebTransportStreamDataReceived) -> None:
|
|
385
|
+
"""Handle WebTransport stream data."""
|
|
386
|
+
await self._process_mcp_request(event.stream_id, event.data)
|
|
387
|
+
|
|
388
|
+
async def _process_mcp_request(self, stream_id: int, data: bytes) -> None:
|
|
389
|
+
"""Process an MCP JSON-RPC request."""
|
|
390
|
+
try:
|
|
391
|
+
# Deserialize request
|
|
392
|
+
request = self.serializer.deserialize(data)
|
|
393
|
+
|
|
394
|
+
method = request.get("method", "")
|
|
395
|
+
params = request.get("params", {})
|
|
396
|
+
request_id = request.get("id")
|
|
397
|
+
|
|
398
|
+
self.logger.info(f"MCP request: {method} (id={request_id})")
|
|
399
|
+
|
|
400
|
+
# Dispatch to handler
|
|
401
|
+
try:
|
|
402
|
+
if method == "initialize":
|
|
403
|
+
result = await self.mcp_handler.handle_initialize(params)
|
|
404
|
+
elif method == "tools/list":
|
|
405
|
+
result = await self.mcp_handler.handle_tools_list(params)
|
|
406
|
+
elif method == "tools/call":
|
|
407
|
+
result = await self.mcp_handler.handle_tools_call(params)
|
|
408
|
+
elif method == "notifications/initialized":
|
|
409
|
+
# Notification - no response needed
|
|
410
|
+
self.logger.info("Client initialization complete")
|
|
411
|
+
return
|
|
412
|
+
else:
|
|
413
|
+
raise RuntimeError(f"Unknown method: {method}")
|
|
414
|
+
|
|
415
|
+
response = {
|
|
416
|
+
"jsonrpc": "2.0",
|
|
417
|
+
"id": request_id,
|
|
418
|
+
"result": result
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
self.logger.error(f"Error handling {method}: {e}")
|
|
423
|
+
response = {
|
|
424
|
+
"jsonrpc": "2.0",
|
|
425
|
+
"id": request_id,
|
|
426
|
+
"error": {
|
|
427
|
+
"code": -32603,
|
|
428
|
+
"message": str(e)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Send response
|
|
433
|
+
response_data = self.serializer.serialize(response)
|
|
434
|
+
self._http.send_data(
|
|
435
|
+
stream_id=stream_id,
|
|
436
|
+
data=response_data,
|
|
437
|
+
end_stream=True
|
|
438
|
+
)
|
|
439
|
+
self.transmit()
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
self.logger.error(f"Failed to process MCP request: {e}")
|
|
443
|
+
error_response = {
|
|
444
|
+
"jsonrpc": "2.0",
|
|
445
|
+
"id": None,
|
|
446
|
+
"error": {
|
|
447
|
+
"code": -32700,
|
|
448
|
+
"message": f"Parse error: {e}"
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
self._http.send_data(
|
|
452
|
+
stream_id=stream_id,
|
|
453
|
+
data=self.serializer.serialize(error_response),
|
|
454
|
+
end_stream=True
|
|
455
|
+
)
|
|
456
|
+
self.transmit()
|
|
457
|
+
|
|
458
|
+
async def _handle_datagram(self, data: bytes) -> None:
|
|
459
|
+
"""Handle unreliable datagram (telemetry, heartbeats)."""
|
|
460
|
+
if not data:
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
msg_type = data[0]
|
|
465
|
+
payload = data[1:]
|
|
466
|
+
|
|
467
|
+
if msg_type == 0x01: # Heartbeat
|
|
468
|
+
self.logger.debug("Received heartbeat datagram")
|
|
469
|
+
# Send heartbeat response
|
|
470
|
+
self._quic.send_datagram_frame(bytes([0x01]))
|
|
471
|
+
self.transmit()
|
|
472
|
+
|
|
473
|
+
elif msg_type == 0x02: # Telemetry
|
|
474
|
+
self.logger.debug(f"Received telemetry: {len(payload)} bytes")
|
|
475
|
+
# Could forward to metrics system
|
|
476
|
+
|
|
477
|
+
elif msg_type == 0x03: # Ping
|
|
478
|
+
# Send pong
|
|
479
|
+
self._quic.send_datagram_frame(bytes([0x04]) + payload)
|
|
480
|
+
self.transmit()
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
self.logger.debug(f"Failed to handle datagram: {e}")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class QuicMCPServer(MCPServerBase):
|
|
487
|
+
"""
|
|
488
|
+
QUIC/HTTP3 MCP Server with WebTransport support.
|
|
489
|
+
|
|
490
|
+
Inherits behavior from MCPServerBase and adds QUIC transport layer.
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> from parrot.mcp.server import MCPServerConfig
|
|
494
|
+
>>>
|
|
495
|
+
>>> config = MCPServerConfig(
|
|
496
|
+
... name="high-perf-mcp",
|
|
497
|
+
... transport="quic",
|
|
498
|
+
... host="0.0.0.0",
|
|
499
|
+
... port=4433,
|
|
500
|
+
... )
|
|
501
|
+
>>>
|
|
502
|
+
>>> server = QuicMCPServer(config)
|
|
503
|
+
>>> server.register_tool(MySearchTool())
|
|
504
|
+
>>> server.register_tool(MyDatabaseTool())
|
|
505
|
+
>>>
|
|
506
|
+
>>> await server.start()
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def __init__(
|
|
510
|
+
self,
|
|
511
|
+
config: MCPServerConfig,
|
|
512
|
+
quic_config: Optional[QuicMCPConfig] = None,
|
|
513
|
+
):
|
|
514
|
+
super().__init__(config)
|
|
515
|
+
# QUIC-specific config
|
|
516
|
+
self.quic_config = quic_config or QuicMCPConfig()
|
|
517
|
+
# Serializer
|
|
518
|
+
self.serializer = MCPSerializer(self.quic_config.serialization)
|
|
519
|
+
# Server state
|
|
520
|
+
self._server: Optional[QuicServer] = None
|
|
521
|
+
self._running = False
|
|
522
|
+
# Session ticket store for 0-RTT
|
|
523
|
+
self._session_tickets: Dict[bytes, SessionTicket] = {}
|
|
524
|
+
# Connected clients tracking
|
|
525
|
+
self._connected_clients: Set[str] = set()
|
|
526
|
+
|
|
527
|
+
async def handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
528
|
+
"""Handle tools/call request."""
|
|
529
|
+
tool_name = params.get("name")
|
|
530
|
+
arguments = params.get("arguments", {})
|
|
531
|
+
|
|
532
|
+
self.logger.info(f"Calling tool: {tool_name}")
|
|
533
|
+
|
|
534
|
+
if tool_name not in self.tools:
|
|
535
|
+
raise RuntimeError(f"Tool not found: {tool_name}")
|
|
536
|
+
|
|
537
|
+
tool = self.tools[tool_name]
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
# Execute tool
|
|
541
|
+
if hasattr(tool, '_execute'):
|
|
542
|
+
result = await tool._execute(**arguments)
|
|
543
|
+
elif hasattr(tool, 'execute'):
|
|
544
|
+
result = await tool.execute(**arguments)
|
|
545
|
+
else:
|
|
546
|
+
result = await tool(**arguments)
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
"content": [{"type": "text", "text": str(result)}],
|
|
550
|
+
"isError": False
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
except Exception as e:
|
|
554
|
+
self.logger.error(f"Tool execution failed: {e}")
|
|
555
|
+
return {
|
|
556
|
+
"content": [{"type": "text", "text": f"Error: {e}"}],
|
|
557
|
+
"isError": True
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# =========================================================================
|
|
561
|
+
# QUIC Server Lifecycle (abstract method implementation)
|
|
562
|
+
# =========================================================================
|
|
563
|
+
|
|
564
|
+
async def start(self) -> None:
|
|
565
|
+
"""Start the QUIC MCP server."""
|
|
566
|
+
self.logger.info(
|
|
567
|
+
f"Starting QUIC MCP server on {self.config.host}:{self.config.port}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Build QUIC configuration
|
|
571
|
+
configuration = QuicConfiguration(
|
|
572
|
+
alpn_protocols=H3_ALPN,
|
|
573
|
+
is_client=False,
|
|
574
|
+
max_datagram_frame_size=self.quic_config.max_datagram_size,
|
|
575
|
+
idle_timeout=self.quic_config.idle_timeout,
|
|
576
|
+
congestion_control_algorithm=self.quic_config.congestion_control,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Load TLS certificates
|
|
580
|
+
cert_path = Path(self.quic_config.cert_path)
|
|
581
|
+
key_path = Path(self.quic_config.key_path)
|
|
582
|
+
|
|
583
|
+
if not cert_path.exists():
|
|
584
|
+
raise FileNotFoundError(
|
|
585
|
+
f"TLS certificate not found: {cert_path}\n"
|
|
586
|
+
f"Generate with: openssl req -x509 -newkey rsa:4096 "
|
|
587
|
+
f"-keyout {key_path} -out {cert_path} -days 365 -nodes"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
configuration.load_cert_chain(str(cert_path), str(key_path))
|
|
591
|
+
|
|
592
|
+
# Enable 0-RTT if configured
|
|
593
|
+
if self.quic_config.enable_0rtt:
|
|
594
|
+
configuration.max_early_data_size = 0xFFFF
|
|
595
|
+
|
|
596
|
+
# Session ticket handler for 0-RTT resumption
|
|
597
|
+
def session_ticket_handler(ticket: SessionTicket) -> None:
|
|
598
|
+
self._session_tickets[ticket.ticket] = ticket
|
|
599
|
+
self.logger.debug("Session ticket stored for 0-RTT resumption")
|
|
600
|
+
|
|
601
|
+
def session_ticket_fetcher(ticket: bytes) -> Optional[SessionTicket]:
|
|
602
|
+
return self._session_tickets.get(ticket)
|
|
603
|
+
|
|
604
|
+
# Create protocol factory
|
|
605
|
+
def create_protocol(*args, **kwargs):
|
|
606
|
+
return QuicMCPServerProtocol(
|
|
607
|
+
*args,
|
|
608
|
+
mcp_handler=self,
|
|
609
|
+
**kwargs
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Start server
|
|
613
|
+
self._server = await quic_serve(
|
|
614
|
+
host=self.config.host,
|
|
615
|
+
port=self.config.port,
|
|
616
|
+
configuration=configuration,
|
|
617
|
+
create_protocol=create_protocol,
|
|
618
|
+
session_ticket_fetcher=session_ticket_fetcher,
|
|
619
|
+
session_ticket_handler=session_ticket_handler,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
self._running = True
|
|
623
|
+
|
|
624
|
+
self.logger.info(
|
|
625
|
+
f"QUIC MCP server started at https://{self.config.host}:{self.config.port}"
|
|
626
|
+
)
|
|
627
|
+
self.logger.info(f"Transport: QUIC/HTTP3 with WebTransport")
|
|
628
|
+
self.logger.info(f"Serialization: {self.quic_config.serialization.value}")
|
|
629
|
+
self.logger.info(f"0-RTT enabled: {self.quic_config.enable_0rtt}")
|
|
630
|
+
self.logger.info(f"Registered tools: {list(self.tools.keys())}")
|
|
631
|
+
|
|
632
|
+
# Keep server running
|
|
633
|
+
try:
|
|
634
|
+
while self._running:
|
|
635
|
+
await asyncio.sleep(1)
|
|
636
|
+
except asyncio.CancelledError:
|
|
637
|
+
pass
|
|
638
|
+
|
|
639
|
+
async def stop(self) -> None:
|
|
640
|
+
"""Stop the QUIC MCP server."""
|
|
641
|
+
self._running = False
|
|
642
|
+
|
|
643
|
+
if self._server:
|
|
644
|
+
self._server.close()
|
|
645
|
+
self._server = None
|
|
646
|
+
|
|
647
|
+
self.logger.info("QUIC MCP server stopped")
|
|
648
|
+
|
|
649
|
+
# =========================================================================
|
|
650
|
+
# Utility Methods
|
|
651
|
+
# =========================================================================
|
|
652
|
+
|
|
653
|
+
def broadcast_datagram(self, data: bytes) -> None:
|
|
654
|
+
"""Broadcast unreliable datagram to all connected clients."""
|
|
655
|
+
# This would iterate over active connections and send datagrams
|
|
656
|
+
pass
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def is_running(self) -> bool:
|
|
660
|
+
return self._running
|
|
661
|
+
|
|
662
|
+
# =============================================================================
|
|
663
|
+
# Client Session
|
|
664
|
+
# =============================================================================
|
|
665
|
+
|
|
666
|
+
class QuicMCPSession:
|
|
667
|
+
"""
|
|
668
|
+
MCP session over QUIC/HTTP3 with WebTransport.
|
|
669
|
+
|
|
670
|
+
Features:
|
|
671
|
+
- 0-RTT connection for minimal latency
|
|
672
|
+
- Multiplexed streams for concurrent tool calls
|
|
673
|
+
- Binary serialization (MessagePack) for efficiency
|
|
674
|
+
- Unreliable datagrams for telemetry
|
|
675
|
+
- Connection migration support
|
|
676
|
+
|
|
677
|
+
Example:
|
|
678
|
+
>>> config = QuicMCPConfig(
|
|
679
|
+
... host="tools.example.com",
|
|
680
|
+
... port=4433,
|
|
681
|
+
... serialization=SerializationFormat.MSGPACK,
|
|
682
|
+
... )
|
|
683
|
+
>>> session = QuicMCPSession(mcp_config, logger)
|
|
684
|
+
>>> await session.connect()
|
|
685
|
+
>>> tools = await session.list_tools()
|
|
686
|
+
>>> result = await session.call_tool("search", {"query": "AI agents"})
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
def __init__(self, config: 'MCPServerConfig', logger: logging.Logger):
|
|
690
|
+
self.config = config
|
|
691
|
+
self.logger = logger
|
|
692
|
+
|
|
693
|
+
# Extract QUIC-specific config
|
|
694
|
+
self.quic_config = config.quic_config or QuicMCPConfig()
|
|
695
|
+
|
|
696
|
+
self._protocol: Optional[QuicMCPClientProtocol] = None
|
|
697
|
+
self._http: Optional[H3Connection] = None
|
|
698
|
+
self._request_id = 0
|
|
699
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
700
|
+
self._initialized = False
|
|
701
|
+
self._session_id: Optional[int] = None
|
|
702
|
+
self._webtransport_stream_id: Optional[int] = None
|
|
703
|
+
|
|
704
|
+
# Serializer
|
|
705
|
+
self.serializer = MCPSerializer(self.quic_config.serialization)
|
|
706
|
+
|
|
707
|
+
# Session ticket for 0-RTT
|
|
708
|
+
self._session_ticket: Optional[SessionTicket] = None
|
|
709
|
+
if self.quic_config.session_ticket_path:
|
|
710
|
+
self._load_session_ticket()
|
|
711
|
+
|
|
712
|
+
def _on_session_ticket(self, ticket: SessionTicket):
|
|
713
|
+
"""Handle new session ticket from server."""
|
|
714
|
+
if self.quic_config.session_ticket_path:
|
|
715
|
+
try:
|
|
716
|
+
import pickle
|
|
717
|
+
path = Path(self.quic_config.session_ticket_path)
|
|
718
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
719
|
+
path.write_bytes(pickle.dumps(ticket))
|
|
720
|
+
except Exception as e:
|
|
721
|
+
self.logger.warning(f"Failed to save session ticket: {e}")
|
|
722
|
+
|
|
723
|
+
def _load_session_ticket(self) -> Optional[SessionTicket]:
|
|
724
|
+
"""Load session ticket for 0-RTT."""
|
|
725
|
+
if self.quic_config.session_ticket_path:
|
|
726
|
+
path = Path(self.quic_config.session_ticket_path)
|
|
727
|
+
if path.exists():
|
|
728
|
+
try:
|
|
729
|
+
import pickle
|
|
730
|
+
return pickle.loads(path.read_bytes())
|
|
731
|
+
except Exception:
|
|
732
|
+
pass
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
def _save_session_ticket(self, ticket: SessionTicket) -> None:
|
|
736
|
+
"""Save session ticket for future 0-RTT connections."""
|
|
737
|
+
if self.quic_config.session_ticket_path:
|
|
738
|
+
try:
|
|
739
|
+
path = Path(self.quic_config.session_ticket_path)
|
|
740
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
741
|
+
path.write_bytes(pickle.dumps(ticket))
|
|
742
|
+
self.logger.info("Saved session ticket for 0-RTT")
|
|
743
|
+
except Exception as e:
|
|
744
|
+
self.logger.warning(f"Failed to save session ticket: {e}")
|
|
745
|
+
|
|
746
|
+
async def connect(self):
|
|
747
|
+
"""Establish QUIC connection to MCP server."""
|
|
748
|
+
try:
|
|
749
|
+
# Build QUIC configuration
|
|
750
|
+
quic_configuration = QuicConfiguration(
|
|
751
|
+
alpn_protocols=H3_ALPN,
|
|
752
|
+
is_client=True,
|
|
753
|
+
max_datagram_frame_size=self.quic_config.max_datagram_size,
|
|
754
|
+
idle_timeout=self.quic_config.idle_timeout,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# TLS configuration
|
|
758
|
+
if self.quic_config.insecure:
|
|
759
|
+
quic_configuration.verify_mode = ssl.CERT_NONE
|
|
760
|
+
else:
|
|
761
|
+
quic_configuration.verify_mode = self.quic_config.verify_mode
|
|
762
|
+
if self.quic_config.ca_cert_path:
|
|
763
|
+
quic_configuration.cafile = self.quic_config.ca_cert_path
|
|
764
|
+
|
|
765
|
+
# Session ticket for 0-RTT
|
|
766
|
+
if self._session_ticket:
|
|
767
|
+
quic_configuration.session_ticket = self._session_ticket
|
|
768
|
+
|
|
769
|
+
# Connect
|
|
770
|
+
self.logger.info(
|
|
771
|
+
f"Connecting to {self.quic_config.host}:{self.quic_config.port} "
|
|
772
|
+
f"via QUIC/HTTP3..."
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
self._protocol = await quic_connect(
|
|
776
|
+
self.quic_config.host,
|
|
777
|
+
self.quic_config.port,
|
|
778
|
+
configuration=quic_configuration,
|
|
779
|
+
create_protocol=QuicMCPClientProtocol,
|
|
780
|
+
session_ticket_handler=self._on_session_ticket,
|
|
781
|
+
)
|
|
782
|
+
# now, configure the http/3
|
|
783
|
+
self._http = H3Connection(
|
|
784
|
+
self._protocol._quic,
|
|
785
|
+
enable_webtransport=self.quic_config.enable_webtransport
|
|
786
|
+
)
|
|
787
|
+
self._protocol._http = self._http
|
|
788
|
+
self._protocol.serializer = self.serializer
|
|
789
|
+
|
|
790
|
+
if self.quic_config.use_webtransport:
|
|
791
|
+
await self._establish_webtransport()
|
|
792
|
+
|
|
793
|
+
# Initialize MCP session
|
|
794
|
+
await self._initialize_session()
|
|
795
|
+
self._initialized = True
|
|
796
|
+
|
|
797
|
+
self.logger.info(
|
|
798
|
+
f"Connected to {self.config.name} via QUIC "
|
|
799
|
+
f"(serialization: {self.quic_config.serialization.value})"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
except Exception as e:
|
|
803
|
+
await self.disconnect()
|
|
804
|
+
raise MCPConnectionError(f"QUIC connection failed: {e}") from e
|
|
805
|
+
|
|
806
|
+
async def _establish_webtransport(self):
|
|
807
|
+
"""Establish WebTransport session over HTTP/3."""
|
|
808
|
+
# Send CONNECT request for WebTransport
|
|
809
|
+
stream_id = self._http._quic.get_next_available_stream_id()
|
|
810
|
+
|
|
811
|
+
headers = [
|
|
812
|
+
(b":method", b"CONNECT"),
|
|
813
|
+
(b":protocol", b"webtransport"),
|
|
814
|
+
(b":scheme", b"https"),
|
|
815
|
+
(b":authority", f"{self.quic_config.host}:{self.quic_config.port}".encode()),
|
|
816
|
+
(b":path", self.quic_config.webtransport_path.encode()),
|
|
817
|
+
(b"sec-webtransport-http3-draft", b"draft02"),
|
|
818
|
+
]
|
|
819
|
+
|
|
820
|
+
self._http.send_headers(stream_id=stream_id, headers=headers)
|
|
821
|
+
self._protocol.transmit()
|
|
822
|
+
# Wait for response
|
|
823
|
+
response_future = asyncio.Future()
|
|
824
|
+
self._protocol._webtransport_futures[stream_id] = response_future
|
|
825
|
+
|
|
826
|
+
try:
|
|
827
|
+
status = await asyncio.wait_for(response_future, timeout=10.0)
|
|
828
|
+
if status != 200:
|
|
829
|
+
raise MCPConnectionError(f"WebTransport rejected: {status}")
|
|
830
|
+
self._session_id = stream_id
|
|
831
|
+
self.logger.debug(
|
|
832
|
+
f"WebTransport session established: {stream_id}"
|
|
833
|
+
)
|
|
834
|
+
except asyncio.TimeoutError:
|
|
835
|
+
raise MCPConnectionError("WebTransport handshake timeout")
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
async def _initialize_session(self):
|
|
839
|
+
"""Initialize MCP session."""
|
|
840
|
+
response = await self.send_request("initialize", {
|
|
841
|
+
"protocolVersion": "2024-11-05",
|
|
842
|
+
"capabilities": {
|
|
843
|
+
"tools": {},
|
|
844
|
+
"experimental": {
|
|
845
|
+
"quicTransport": True,
|
|
846
|
+
"binarySerialization": self.quic_config.serialization.value,
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
"clientInfo": {
|
|
850
|
+
"name": "ai-parrot",
|
|
851
|
+
"version": "1.0.0",
|
|
852
|
+
"transport": "quic"
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
if "error" in response:
|
|
857
|
+
raise MCPConnectionError(f"Initialize failed: {response['error']}")
|
|
858
|
+
|
|
859
|
+
# Send initialized notification
|
|
860
|
+
await self.send_notification("notifications/initialized")
|
|
861
|
+
|
|
862
|
+
self.logger.info(f"MCP session initialized over QUIC")
|
|
863
|
+
|
|
864
|
+
async def send_request(
|
|
865
|
+
self,
|
|
866
|
+
method: str,
|
|
867
|
+
params: Optional[Dict] = None,
|
|
868
|
+
timeout: float = 30.0
|
|
869
|
+
) -> Dict[str, Any]:
|
|
870
|
+
"""Send JSON-RPC request over QUIC stream."""
|
|
871
|
+
self._request_id += 1
|
|
872
|
+
|
|
873
|
+
request = {
|
|
874
|
+
"jsonrpc": "2.0",
|
|
875
|
+
"id": self._request_id,
|
|
876
|
+
"method": method,
|
|
877
|
+
"params": params or {}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
# Serialize
|
|
881
|
+
data = self.serializer.serialize(request)
|
|
882
|
+
|
|
883
|
+
# Create a new stream for this request (multiplexed!)
|
|
884
|
+
if self.quic_config.use_webtransport and self._session_id:
|
|
885
|
+
stream_id = self._http.create_webtransport_stream(
|
|
886
|
+
session_id=self._session_id,
|
|
887
|
+
is_unidirectional=False
|
|
888
|
+
)
|
|
889
|
+
else:
|
|
890
|
+
stream_id = self._http._quic.get_next_available_stream_id()
|
|
891
|
+
|
|
892
|
+
# Send data
|
|
893
|
+
self._http.send_data(stream_id=stream_id, data=data, end_stream=True)
|
|
894
|
+
self._protocol.transmit()
|
|
895
|
+
|
|
896
|
+
# Wait for response
|
|
897
|
+
future = asyncio.get_event_loop().create_future()
|
|
898
|
+
self._pending_requests[self._request_id] = future
|
|
899
|
+
self._protocol._pending_requests[self._request_id] = future
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
response = await asyncio.wait_for(future, timeout=timeout)
|
|
903
|
+
return response
|
|
904
|
+
except asyncio.TimeoutError:
|
|
905
|
+
self._pending_requests.pop(self._request_id, None)
|
|
906
|
+
raise TimeoutError(f"Request {method} timed out after {timeout}s")
|
|
907
|
+
|
|
908
|
+
async def send_notification(self, method: str, params: Optional[Dict] = None):
|
|
909
|
+
"""Send one-way notification (no response expected)."""
|
|
910
|
+
notification = {
|
|
911
|
+
"jsonrpc": "2.0",
|
|
912
|
+
"method": method,
|
|
913
|
+
"params": params or {}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
data = self.serializer.serialize(notification)
|
|
917
|
+
|
|
918
|
+
# Use unidirectional stream for notifications
|
|
919
|
+
if self.quic_config.use_webtransport and self._session_id:
|
|
920
|
+
stream_id = self._http.create_webtransport_stream(
|
|
921
|
+
session_id=self._session_id,
|
|
922
|
+
is_unidirectional=True
|
|
923
|
+
)
|
|
924
|
+
else:
|
|
925
|
+
stream_id = self._http._quic.get_next_available_stream_id()
|
|
926
|
+
|
|
927
|
+
self._http.send_data(stream_id=stream_id, data=data, end_stream=True)
|
|
928
|
+
self._protocol.transmit()
|
|
929
|
+
|
|
930
|
+
def send_telemetry(self, data: bytes):
|
|
931
|
+
"""Send unreliable telemetry via datagram."""
|
|
932
|
+
if self._protocol:
|
|
933
|
+
# Prefix with type byte
|
|
934
|
+
datagram = bytes([0x02]) + data
|
|
935
|
+
self._protocol._quic.send_datagram_frame(datagram)
|
|
936
|
+
self._protocol.transmit()
|
|
937
|
+
|
|
938
|
+
async def list_tools(self) -> List[Dict]:
|
|
939
|
+
"""List available tools from MCP server."""
|
|
940
|
+
response = await self.send_request("tools/list")
|
|
941
|
+
return response.get("result", {}).get("tools", [])
|
|
942
|
+
|
|
943
|
+
async def call_tool(self, name: str, arguments: Dict) -> Dict:
|
|
944
|
+
"""Call a tool on the MCP server."""
|
|
945
|
+
response = await self.send_request("tools/call", {
|
|
946
|
+
"name": name,
|
|
947
|
+
"arguments": arguments
|
|
948
|
+
})
|
|
949
|
+
return response.get("result", {})
|
|
950
|
+
|
|
951
|
+
async def disconnect(self):
|
|
952
|
+
"""Disconnect from MCP server."""
|
|
953
|
+
self._initialized = False
|
|
954
|
+
|
|
955
|
+
if self._protocol:
|
|
956
|
+
self._protocol._quic.close()
|
|
957
|
+
self._protocol = None
|
|
958
|
+
|
|
959
|
+
self._http = None
|
|
960
|
+
self.logger.info("QUIC connection closed")
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def is_connected(self) -> bool:
|
|
964
|
+
return self._initialized and self._protocol is not None
|
|
965
|
+
|
|
966
|
+
async def __aenter__(self):
|
|
967
|
+
await self.connect()
|
|
968
|
+
return self
|
|
969
|
+
|
|
970
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
971
|
+
await self.disconnect()
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
# =============================================================================
|
|
976
|
+
# Factory Function
|
|
977
|
+
# =============================================================================
|
|
978
|
+
# Añadir a parrot/mcp/integration.py
|
|
979
|
+
|
|
980
|
+
def create_quic_mcp_server(
|
|
981
|
+
name: str,
|
|
982
|
+
host: str,
|
|
983
|
+
port: int = 4433,
|
|
984
|
+
*,
|
|
985
|
+
cert_path: Optional[str] = None,
|
|
986
|
+
ca_cert_path: Optional[str] = None,
|
|
987
|
+
insecure: bool = False,
|
|
988
|
+
serialization: str = "msgpack",
|
|
989
|
+
enable_0rtt: bool = True,
|
|
990
|
+
session_ticket_path: Optional[str] = None,
|
|
991
|
+
**kwargs
|
|
992
|
+
) -> MCPServerConfig:
|
|
993
|
+
"""
|
|
994
|
+
Create configuration for QUIC/HTTP3 MCP server.
|
|
995
|
+
|
|
996
|
+
This transport provides:
|
|
997
|
+
- ~40% lower latency than HTTP/SSE (0-RTT connection)
|
|
998
|
+
- ~60% smaller messages (MessagePack serialization)
|
|
999
|
+
- True multiplexing without head-of-line blocking
|
|
1000
|
+
- Connection migration for mobile agents
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
name: Server name for tool prefixing
|
|
1004
|
+
host: Server hostname
|
|
1005
|
+
port: Server port (default 4433, standard QUIC port)
|
|
1006
|
+
cert_path: Path to TLS certificate
|
|
1007
|
+
ca_cert_path: Path to CA certificate for verification
|
|
1008
|
+
insecure: Skip certificate verification (dev only!)
|
|
1009
|
+
serialization: "json", "msgpack", or "protobuf"
|
|
1010
|
+
enable_0rtt: Enable 0-RTT fast reconnection
|
|
1011
|
+
session_ticket_path: Path to store session tickets for 0-RTT
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
MCPServerConfig configured for QUIC transport
|
|
1015
|
+
|
|
1016
|
+
Example:
|
|
1017
|
+
>>> # Production setup
|
|
1018
|
+
>>> config = create_quic_mcp_server(
|
|
1019
|
+
... "ml-inference",
|
|
1020
|
+
... host="ml-cluster.internal.example.com",
|
|
1021
|
+
... port=4433,
|
|
1022
|
+
... ca_cert_path="/etc/ssl/ca-bundle.crt",
|
|
1023
|
+
... serialization="msgpack",
|
|
1024
|
+
... )
|
|
1025
|
+
>>>
|
|
1026
|
+
>>> # Development with self-signed cert
|
|
1027
|
+
>>> config = create_quic_mcp_server(
|
|
1028
|
+
... "local-tools",
|
|
1029
|
+
... host="localhost",
|
|
1030
|
+
... port=4433,
|
|
1031
|
+
... insecure=True, # Only for development!
|
|
1032
|
+
... )
|
|
1033
|
+
>>>
|
|
1034
|
+
>>> await agent.add_mcp_server(config)
|
|
1035
|
+
"""
|
|
1036
|
+
quic_config = QuicMCPConfig(
|
|
1037
|
+
host=host,
|
|
1038
|
+
port=port,
|
|
1039
|
+
cert_path=cert_path,
|
|
1040
|
+
ca_cert_path=ca_cert_path,
|
|
1041
|
+
insecure=insecure,
|
|
1042
|
+
serialization=SerializationFormat(serialization),
|
|
1043
|
+
enable_0rtt=enable_0rtt,
|
|
1044
|
+
session_ticket_path=session_ticket_path,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
return MCPServerConfig(
|
|
1048
|
+
name=name,
|
|
1049
|
+
transport="quic",
|
|
1050
|
+
quic_config=quic_config,
|
|
1051
|
+
**kwargs
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
# =============================================================================
|
|
1056
|
+
# Certificate Generation Helper
|
|
1057
|
+
# =============================================================================
|
|
1058
|
+
def generate_self_signed_cert(
|
|
1059
|
+
cert_path: str = "cert.pem",
|
|
1060
|
+
key_path: str = "key.pem",
|
|
1061
|
+
hostname: str = "localhost",
|
|
1062
|
+
days: int = 365,
|
|
1063
|
+
) -> None:
|
|
1064
|
+
"""
|
|
1065
|
+
Generate self-signed certificate for development.
|
|
1066
|
+
For production, use proper certificates from a CA.
|
|
1067
|
+
"""
|
|
1068
|
+
import subprocess
|
|
1069
|
+
|
|
1070
|
+
cmd = [
|
|
1071
|
+
"openssl", "req", "-x509",
|
|
1072
|
+
"-newkey", "rsa:4096",
|
|
1073
|
+
"-keyout", key_path,
|
|
1074
|
+
"-out", cert_path,
|
|
1075
|
+
"-days", str(days),
|
|
1076
|
+
"-nodes",
|
|
1077
|
+
"-subj", f"/CN={hostname}",
|
|
1078
|
+
"-addext", f"subjectAltName=DNS:{hostname},IP:127.0.0.1",
|
|
1079
|
+
]
|
|
1080
|
+
|
|
1081
|
+
subprocess.run(cmd, check=True)
|
|
1082
|
+
print(f"Generated: {cert_path}, {key_path}")
|