PraisonAI 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth 2.1 Implementation for MCP
|
|
3
|
+
|
|
4
|
+
Implements OAuth 2.1 authorization framework per MCP 2025-11-25 specification.
|
|
5
|
+
|
|
6
|
+
Based on:
|
|
7
|
+
- OAuth 2.1 IETF DRAFT (draft-ietf-oauth-v2-1-13)
|
|
8
|
+
- OAuth 2.0 Authorization Server Metadata (RFC8414)
|
|
9
|
+
- OAuth 2.0 Dynamic Client Registration Protocol (RFC7591)
|
|
10
|
+
- OAuth 2.0 Protected Resource Metadata (RFC9728)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import hashlib
|
|
15
|
+
import logging
|
|
16
|
+
import secrets
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
20
|
+
from urllib.parse import urlencode
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class OAuthConfig:
|
|
27
|
+
"""OAuth 2.1 configuration for MCP servers."""
|
|
28
|
+
|
|
29
|
+
# Authorization server endpoints
|
|
30
|
+
authorization_endpoint: str
|
|
31
|
+
token_endpoint: str
|
|
32
|
+
|
|
33
|
+
# Client credentials
|
|
34
|
+
client_id: str
|
|
35
|
+
client_secret: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
# Scopes
|
|
38
|
+
scopes: List[str] = field(default_factory=list)
|
|
39
|
+
default_scopes: List[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
# PKCE settings (required for OAuth 2.1)
|
|
42
|
+
use_pkce: bool = True
|
|
43
|
+
pkce_method: str = "S256" # S256 or plain
|
|
44
|
+
|
|
45
|
+
# Token settings
|
|
46
|
+
token_endpoint_auth_method: str = "client_secret_basic"
|
|
47
|
+
|
|
48
|
+
# Discovery
|
|
49
|
+
issuer: Optional[str] = None
|
|
50
|
+
metadata_url: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
# Resource indicator (RFC 8707)
|
|
53
|
+
resource_indicator: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
# Redirect settings
|
|
56
|
+
redirect_uri: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"authorization_endpoint": self.authorization_endpoint,
|
|
61
|
+
"token_endpoint": self.token_endpoint,
|
|
62
|
+
"client_id": self.client_id,
|
|
63
|
+
"scopes": self.scopes,
|
|
64
|
+
"use_pkce": self.use_pkce,
|
|
65
|
+
"issuer": self.issuer,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class TokenResponse:
|
|
71
|
+
"""OAuth token response."""
|
|
72
|
+
access_token: str
|
|
73
|
+
token_type: str = "Bearer"
|
|
74
|
+
expires_in: Optional[int] = None
|
|
75
|
+
refresh_token: Optional[str] = None
|
|
76
|
+
scope: Optional[str] = None
|
|
77
|
+
id_token: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
# Computed fields
|
|
80
|
+
expires_at: Optional[float] = None
|
|
81
|
+
|
|
82
|
+
def __post_init__(self):
|
|
83
|
+
if self.expires_in and not self.expires_at:
|
|
84
|
+
self.expires_at = time.time() + self.expires_in
|
|
85
|
+
|
|
86
|
+
def is_expired(self) -> bool:
|
|
87
|
+
if self.expires_at is None:
|
|
88
|
+
return False
|
|
89
|
+
return time.time() >= self.expires_at
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
92
|
+
result = {
|
|
93
|
+
"access_token": self.access_token,
|
|
94
|
+
"token_type": self.token_type,
|
|
95
|
+
}
|
|
96
|
+
if self.expires_in:
|
|
97
|
+
result["expires_in"] = self.expires_in
|
|
98
|
+
if self.refresh_token:
|
|
99
|
+
result["refresh_token"] = self.refresh_token
|
|
100
|
+
if self.scope:
|
|
101
|
+
result["scope"] = self.scope
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class AuthorizationRequest:
|
|
107
|
+
"""OAuth authorization request state."""
|
|
108
|
+
state: str
|
|
109
|
+
code_verifier: Optional[str] = None
|
|
110
|
+
code_challenge: Optional[str] = None
|
|
111
|
+
scopes: List[str] = field(default_factory=list)
|
|
112
|
+
redirect_uri: Optional[str] = None
|
|
113
|
+
created_at: float = field(default_factory=time.time)
|
|
114
|
+
|
|
115
|
+
def is_expired(self, ttl: int = 600) -> bool:
|
|
116
|
+
return time.time() - self.created_at > ttl
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OAuthManager:
|
|
120
|
+
"""
|
|
121
|
+
OAuth 2.1 Manager for MCP servers.
|
|
122
|
+
|
|
123
|
+
Handles:
|
|
124
|
+
- Authorization code flow with PKCE
|
|
125
|
+
- Token exchange and refresh
|
|
126
|
+
- Incremental scope consent
|
|
127
|
+
- WWW-Authenticate challenges
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
config: OAuthConfig,
|
|
133
|
+
token_store: Optional[Callable] = None,
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Initialize OAuth manager.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config: OAuth configuration
|
|
140
|
+
token_store: Optional token storage callback
|
|
141
|
+
"""
|
|
142
|
+
self.config = config
|
|
143
|
+
self._token_store = token_store
|
|
144
|
+
self._pending_requests: Dict[str, AuthorizationRequest] = {}
|
|
145
|
+
self._tokens: Dict[str, TokenResponse] = {}
|
|
146
|
+
|
|
147
|
+
def create_authorization_url(
|
|
148
|
+
self,
|
|
149
|
+
scopes: Optional[List[str]] = None,
|
|
150
|
+
state: Optional[str] = None,
|
|
151
|
+
redirect_uri: Optional[str] = None,
|
|
152
|
+
) -> tuple[str, AuthorizationRequest]:
|
|
153
|
+
"""
|
|
154
|
+
Create authorization URL for OAuth flow.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
scopes: Requested scopes
|
|
158
|
+
state: Optional state parameter
|
|
159
|
+
redirect_uri: Redirect URI
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Tuple of (authorization_url, request_state)
|
|
163
|
+
"""
|
|
164
|
+
# Generate state if not provided
|
|
165
|
+
state = state or secrets.token_urlsafe(32)
|
|
166
|
+
|
|
167
|
+
# Use default scopes if not specified
|
|
168
|
+
scopes = scopes or self.config.default_scopes
|
|
169
|
+
|
|
170
|
+
# Generate PKCE parameters
|
|
171
|
+
code_verifier = None
|
|
172
|
+
code_challenge = None
|
|
173
|
+
|
|
174
|
+
if self.config.use_pkce:
|
|
175
|
+
code_verifier = secrets.token_urlsafe(64)
|
|
176
|
+
if self.config.pkce_method == "S256":
|
|
177
|
+
code_challenge = base64.urlsafe_b64encode(
|
|
178
|
+
hashlib.sha256(code_verifier.encode()).digest()
|
|
179
|
+
).decode().rstrip("=")
|
|
180
|
+
else:
|
|
181
|
+
code_challenge = code_verifier
|
|
182
|
+
|
|
183
|
+
# Build authorization URL
|
|
184
|
+
params = {
|
|
185
|
+
"response_type": "code",
|
|
186
|
+
"client_id": self.config.client_id,
|
|
187
|
+
"state": state,
|
|
188
|
+
"scope": " ".join(scopes),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if redirect_uri or self.config.redirect_uri:
|
|
192
|
+
params["redirect_uri"] = redirect_uri or self.config.redirect_uri
|
|
193
|
+
|
|
194
|
+
if code_challenge:
|
|
195
|
+
params["code_challenge"] = code_challenge
|
|
196
|
+
params["code_challenge_method"] = self.config.pkce_method
|
|
197
|
+
|
|
198
|
+
if self.config.resource_indicator:
|
|
199
|
+
params["resource"] = self.config.resource_indicator
|
|
200
|
+
|
|
201
|
+
auth_url = f"{self.config.authorization_endpoint}?{urlencode(params)}"
|
|
202
|
+
|
|
203
|
+
# Store request state
|
|
204
|
+
request = AuthorizationRequest(
|
|
205
|
+
state=state,
|
|
206
|
+
code_verifier=code_verifier,
|
|
207
|
+
code_challenge=code_challenge,
|
|
208
|
+
scopes=scopes,
|
|
209
|
+
redirect_uri=redirect_uri or self.config.redirect_uri,
|
|
210
|
+
)
|
|
211
|
+
self._pending_requests[state] = request
|
|
212
|
+
|
|
213
|
+
return auth_url, request
|
|
214
|
+
|
|
215
|
+
async def exchange_code(
|
|
216
|
+
self,
|
|
217
|
+
code: str,
|
|
218
|
+
state: str,
|
|
219
|
+
) -> TokenResponse:
|
|
220
|
+
"""
|
|
221
|
+
Exchange authorization code for tokens.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
code: Authorization code
|
|
225
|
+
state: State parameter
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Token response
|
|
229
|
+
"""
|
|
230
|
+
# Validate state
|
|
231
|
+
request = self._pending_requests.get(state)
|
|
232
|
+
if not request:
|
|
233
|
+
raise ValueError("Invalid state parameter")
|
|
234
|
+
|
|
235
|
+
if request.is_expired():
|
|
236
|
+
del self._pending_requests[state]
|
|
237
|
+
raise ValueError("Authorization request expired")
|
|
238
|
+
|
|
239
|
+
# Build token request
|
|
240
|
+
data = {
|
|
241
|
+
"grant_type": "authorization_code",
|
|
242
|
+
"code": code,
|
|
243
|
+
"client_id": self.config.client_id,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if request.redirect_uri:
|
|
247
|
+
data["redirect_uri"] = request.redirect_uri
|
|
248
|
+
|
|
249
|
+
if request.code_verifier:
|
|
250
|
+
data["code_verifier"] = request.code_verifier
|
|
251
|
+
|
|
252
|
+
# Make token request
|
|
253
|
+
token_response = await self._token_request(data)
|
|
254
|
+
|
|
255
|
+
# Cleanup
|
|
256
|
+
del self._pending_requests[state]
|
|
257
|
+
|
|
258
|
+
return token_response
|
|
259
|
+
|
|
260
|
+
async def refresh_token(
|
|
261
|
+
self,
|
|
262
|
+
refresh_token: str,
|
|
263
|
+
scopes: Optional[List[str]] = None,
|
|
264
|
+
) -> TokenResponse:
|
|
265
|
+
"""
|
|
266
|
+
Refresh an access token.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
refresh_token: Refresh token
|
|
270
|
+
scopes: Optional scope restriction
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
New token response
|
|
274
|
+
"""
|
|
275
|
+
data = {
|
|
276
|
+
"grant_type": "refresh_token",
|
|
277
|
+
"refresh_token": refresh_token,
|
|
278
|
+
"client_id": self.config.client_id,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if scopes:
|
|
282
|
+
data["scope"] = " ".join(scopes)
|
|
283
|
+
|
|
284
|
+
return await self._token_request(data)
|
|
285
|
+
|
|
286
|
+
async def _token_request(self, data: Dict[str, str]) -> TokenResponse:
|
|
287
|
+
"""Make a token request."""
|
|
288
|
+
try:
|
|
289
|
+
import httpx
|
|
290
|
+
except ImportError:
|
|
291
|
+
raise ImportError("httpx required for OAuth. Install with: pip install httpx")
|
|
292
|
+
|
|
293
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
294
|
+
|
|
295
|
+
# Add client authentication
|
|
296
|
+
if self.config.token_endpoint_auth_method == "client_secret_basic":
|
|
297
|
+
if self.config.client_secret:
|
|
298
|
+
credentials = base64.b64encode(
|
|
299
|
+
f"{self.config.client_id}:{self.config.client_secret}".encode()
|
|
300
|
+
).decode()
|
|
301
|
+
headers["Authorization"] = f"Basic {credentials}"
|
|
302
|
+
elif self.config.token_endpoint_auth_method == "client_secret_post":
|
|
303
|
+
if self.config.client_secret:
|
|
304
|
+
data["client_secret"] = self.config.client_secret
|
|
305
|
+
|
|
306
|
+
async with httpx.AsyncClient() as client:
|
|
307
|
+
response = await client.post(
|
|
308
|
+
self.config.token_endpoint,
|
|
309
|
+
data=data,
|
|
310
|
+
headers=headers,
|
|
311
|
+
)
|
|
312
|
+
response.raise_for_status()
|
|
313
|
+
|
|
314
|
+
token_data = response.json()
|
|
315
|
+
return TokenResponse(
|
|
316
|
+
access_token=token_data["access_token"],
|
|
317
|
+
token_type=token_data.get("token_type", "Bearer"),
|
|
318
|
+
expires_in=token_data.get("expires_in"),
|
|
319
|
+
refresh_token=token_data.get("refresh_token"),
|
|
320
|
+
scope=token_data.get("scope"),
|
|
321
|
+
id_token=token_data.get("id_token"),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def create_www_authenticate_challenge(
|
|
325
|
+
self,
|
|
326
|
+
required_scopes: List[str],
|
|
327
|
+
error: Optional[str] = None,
|
|
328
|
+
error_description: Optional[str] = None,
|
|
329
|
+
) -> str:
|
|
330
|
+
"""
|
|
331
|
+
Create WWW-Authenticate header for scope challenge.
|
|
332
|
+
|
|
333
|
+
Per MCP 2025-11-25, this enables incremental scope consent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
required_scopes: Scopes required for the operation
|
|
337
|
+
error: OAuth error code
|
|
338
|
+
error_description: Human-readable error description
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
WWW-Authenticate header value
|
|
342
|
+
"""
|
|
343
|
+
parts = ['Bearer']
|
|
344
|
+
|
|
345
|
+
params = []
|
|
346
|
+
|
|
347
|
+
if self.config.issuer:
|
|
348
|
+
params.append(f'realm="{self.config.issuer}"')
|
|
349
|
+
|
|
350
|
+
if required_scopes:
|
|
351
|
+
params.append(f'scope="{" ".join(required_scopes)}"')
|
|
352
|
+
|
|
353
|
+
if error:
|
|
354
|
+
params.append(f'error="{error}"')
|
|
355
|
+
|
|
356
|
+
if error_description:
|
|
357
|
+
params.append(f'error_description="{error_description}"')
|
|
358
|
+
|
|
359
|
+
if params:
|
|
360
|
+
parts.append(", ".join(params))
|
|
361
|
+
|
|
362
|
+
return " ".join(parts)
|
|
363
|
+
|
|
364
|
+
def validate_token(self, token: str) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Validate an access token.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
token: Access token to validate
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
True if valid
|
|
373
|
+
"""
|
|
374
|
+
# Check if we have this token stored
|
|
375
|
+
for stored_token in self._tokens.values():
|
|
376
|
+
if stored_token.access_token == token:
|
|
377
|
+
return not stored_token.is_expired()
|
|
378
|
+
|
|
379
|
+
# For external tokens, we'd need to call the introspection endpoint
|
|
380
|
+
# This is a simplified implementation
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
def store_token(self, session_id: str, token: TokenResponse) -> None:
|
|
384
|
+
"""Store a token for a session."""
|
|
385
|
+
self._tokens[session_id] = token
|
|
386
|
+
if self._token_store:
|
|
387
|
+
self._token_store(session_id, token)
|
|
388
|
+
|
|
389
|
+
def get_token(self, session_id: str) -> Optional[TokenResponse]:
|
|
390
|
+
"""Get stored token for a session."""
|
|
391
|
+
return self._tokens.get(session_id)
|
|
392
|
+
|
|
393
|
+
def clear_token(self, session_id: str) -> None:
|
|
394
|
+
"""Clear stored token for a session."""
|
|
395
|
+
if session_id in self._tokens:
|
|
396
|
+
del self._tokens[session_id]
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def discover_oauth_metadata(issuer_url: str) -> Dict[str, Any]:
|
|
400
|
+
"""
|
|
401
|
+
Discover OAuth authorization server metadata.
|
|
402
|
+
|
|
403
|
+
Per RFC 8414, fetches from /.well-known/oauth-authorization-server
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
issuer_url: Authorization server issuer URL
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Metadata dictionary
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
import httpx
|
|
413
|
+
except ImportError:
|
|
414
|
+
raise ImportError("httpx required for OAuth discovery. Install with: pip install httpx")
|
|
415
|
+
|
|
416
|
+
# Try OAuth 2.0 metadata endpoint first
|
|
417
|
+
metadata_url = f"{issuer_url.rstrip('/')}/.well-known/oauth-authorization-server"
|
|
418
|
+
|
|
419
|
+
async with httpx.AsyncClient() as client:
|
|
420
|
+
try:
|
|
421
|
+
response = await client.get(metadata_url)
|
|
422
|
+
if response.status_code == 200:
|
|
423
|
+
return response.json()
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
# Fall back to OpenID Connect discovery
|
|
428
|
+
oidc_url = f"{issuer_url.rstrip('/')}/.well-known/openid-configuration"
|
|
429
|
+
response = await client.get(oidc_url)
|
|
430
|
+
response.raise_for_status()
|
|
431
|
+
return response.json()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def create_oauth_config_from_metadata(
|
|
435
|
+
metadata: Dict[str, Any],
|
|
436
|
+
client_id: str,
|
|
437
|
+
client_secret: Optional[str] = None,
|
|
438
|
+
scopes: Optional[List[str]] = None,
|
|
439
|
+
) -> OAuthConfig:
|
|
440
|
+
"""
|
|
441
|
+
Create OAuth config from discovered metadata.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
metadata: Authorization server metadata
|
|
445
|
+
client_id: Client ID
|
|
446
|
+
client_secret: Optional client secret
|
|
447
|
+
scopes: Optional scopes
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
OAuthConfig instance
|
|
451
|
+
"""
|
|
452
|
+
return OAuthConfig(
|
|
453
|
+
authorization_endpoint=metadata["authorization_endpoint"],
|
|
454
|
+
token_endpoint=metadata["token_endpoint"],
|
|
455
|
+
client_id=client_id,
|
|
456
|
+
client_secret=client_secret,
|
|
457
|
+
scopes=scopes or metadata.get("scopes_supported", []),
|
|
458
|
+
issuer=metadata.get("issuer"),
|
|
459
|
+
metadata_url=metadata.get("issuer"),
|
|
460
|
+
)
|