agora-workbench 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. agora_workbench-0.1.1/LICENSE +21 -0
  2. agora_workbench-0.1.1/MANIFEST.in +6 -0
  3. agora_workbench-0.1.1/PKG-INFO +189 -0
  4. agora_workbench-0.1.1/README.md +140 -0
  5. agora_workbench-0.1.1/activity_ui/Dockerfile +21 -0
  6. agora_workbench-0.1.1/activity_ui/__init__.py +5 -0
  7. agora_workbench-0.1.1/activity_ui/auth.py +298 -0
  8. agora_workbench-0.1.1/activity_ui/docker-compose.yml +43 -0
  9. agora_workbench-0.1.1/activity_ui/models.py +109 -0
  10. agora_workbench-0.1.1/activity_ui/requirements.txt +5 -0
  11. agora_workbench-0.1.1/activity_ui/server.py +198 -0
  12. agora_workbench-0.1.1/activity_ui/static/index.html +901 -0
  13. agora_workbench-0.1.1/pyproject.toml +145 -0
  14. agora_workbench-0.1.1/setup.cfg +4 -0
  15. agora_workbench-0.1.1/src/agora_workbench/__init__.py +57 -0
  16. agora_workbench-0.1.1/src/agora_workbench/base/__init__.py +416 -0
  17. agora_workbench-0.1.1/src/agora_workbench/code_execution/__init__.py +31 -0
  18. agora_workbench-0.1.1/src/agora_workbench/code_execution/activity_publisher.py +136 -0
  19. agora_workbench-0.1.1/src/agora_workbench/code_execution/agent_guidance.py +115 -0
  20. agora_workbench-0.1.1/src/agora_workbench/code_execution/asset_provisioner.py +200 -0
  21. agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/__init__.py +70 -0
  22. agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/azure_credentials.py +207 -0
  23. agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/base.py +165 -0
  24. agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/entra.py +232 -0
  25. agora_workbench-0.1.1/src/agora_workbench/code_execution/auth/noop.py +166 -0
  26. agora_workbench-0.1.1/src/agora_workbench/code_execution/catalog_tools.py +202 -0
  27. agora_workbench-0.1.1/src/agora_workbench/code_execution/cli.py +157 -0
  28. agora_workbench-0.1.1/src/agora_workbench/code_execution/code_execution.py +972 -0
  29. agora_workbench-0.1.1/src/agora_workbench/code_execution/code_execution_models.py +433 -0
  30. agora_workbench-0.1.1/src/agora_workbench/code_execution/code_extraction.py +211 -0
  31. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/__init__.py +38 -0
  32. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/__init__.py +13 -0
  33. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/config.py +87 -0
  34. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/db.py +342 -0
  35. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/embeddings.py +117 -0
  36. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/catalog/indexer.py +365 -0
  37. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/credentials.py +189 -0
  38. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/fetchers.py +444 -0
  39. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/manager.py +387 -0
  40. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/publishers.py +552 -0
  41. agora_workbench-0.1.1/src/agora_workbench/code_execution/data_access/resolution.py +195 -0
  42. agora_workbench-0.1.1/src/agora_workbench/code_execution/environment_builders.py +420 -0
  43. agora_workbench-0.1.1/src/agora_workbench/code_execution/object_transfer.py +199 -0
  44. agora_workbench-0.1.1/src/agora_workbench/code_execution/server.py +2896 -0
  45. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/__init__.py +97 -0
  46. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/context.py +176 -0
  47. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/manager.py +1492 -0
  48. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/mcp_integration.py +139 -0
  49. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/meta_tools.py +214 -0
  50. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/objects.py +75 -0
  51. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/session.py +140 -0
  52. agora_workbench-0.1.1/src/agora_workbench/code_execution/sessions/storage.py +68 -0
  53. agora_workbench-0.1.1/src/agora_workbench/code_execution/sidecar.py +226 -0
  54. agora_workbench-0.1.1/src/agora_workbench/code_execution/skills.py +126 -0
  55. agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_proxy.py +288 -0
  56. agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/__init__.py +13 -0
  57. agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/tool_registry.py +205 -0
  58. agora_workbench-0.1.1/src/agora_workbench/code_execution/tool_registry/tool_schema.py +274 -0
  59. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/__init__.py +23 -0
  60. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/__init__.py +65 -0
  61. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_bm25/__init__.py +5 -0
  62. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_bm25/index.py +119 -0
  63. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/_constants.py +6 -0
  64. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/azure_ai_tool_search.py +516 -0
  65. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/bm25_tool_search.py +139 -0
  66. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/state_graph.py +547 -0
  67. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/search/state_graph_tools.py +260 -0
  68. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/tool_descriptor.py +57 -0
  69. agora_workbench-0.1.1/src/agora_workbench/code_execution/tools/tool_search.py +103 -0
  70. agora_workbench-0.1.1/src/agora_workbench/code_execution/types.py +22 -0
  71. agora_workbench-0.1.1/src/agora_workbench/connector/__init__.py +38 -0
  72. agora_workbench-0.1.1/src/agora_workbench/connector/__main__.py +6 -0
  73. agora_workbench-0.1.1/src/agora_workbench/connector/base.py +906 -0
  74. agora_workbench-0.1.1/src/agora_workbench/connector/cli.py +303 -0
  75. agora_workbench-0.1.1/src/agora_workbench/connector/dispatcher.py +576 -0
  76. agora_workbench-0.1.1/src/agora_workbench/connector/gateway.py +155 -0
  77. agora_workbench-0.1.1/src/agora_workbench/connector/models.py +209 -0
  78. agora_workbench-0.1.1/src/agora_workbench/connector/router.py +196 -0
  79. agora_workbench-0.1.1/src/agora_workbench/deployment/__init__.py +0 -0
  80. agora_workbench-0.1.1/src/agora_workbench/deployment/cli.py +184 -0
  81. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/__init__.py +0 -0
  82. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/activity_ui/README.md +19 -0
  83. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/README.md +360 -0
  84. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/__init__.py +0 -0
  85. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/_deploy-common.sh +106 -0
  86. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/activity-ui.bicep +202 -0
  87. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy-network.sh +396 -0
  88. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy-server.sh +270 -0
  89. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/deploy.sh +42 -0
  90. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/main.bicep +232 -0
  91. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/networks/__init__.py +0 -0
  92. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/networks/router.yaml +20 -0
  93. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/parameters/__init__.py +0 -0
  94. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/parameters/server.bicepparam +21 -0
  95. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/setup-app-registrations.sh +325 -0
  96. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/azure/setup.sh +192 -0
  97. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/Dockerfile +26 -0
  98. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/__init__.py +0 -0
  99. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/base.Dockerfile +105 -0
  100. agora_workbench-0.1.1/src/agora_workbench/deployment/templates/docker/docker-compose.yml +75 -0
  101. agora_workbench-0.1.1/src/agora_workbench.egg-info/PKG-INFO +189 -0
  102. agora_workbench-0.1.1/src/agora_workbench.egg-info/SOURCES.txt +104 -0
  103. agora_workbench-0.1.1/src/agora_workbench.egg-info/dependency_links.txt +1 -0
  104. agora_workbench-0.1.1/src/agora_workbench.egg-info/entry_points.txt +3 -0
  105. agora_workbench-0.1.1/src/agora_workbench.egg-info/requires.txt +38 -0
  106. agora_workbench-0.1.1/src/agora_workbench.egg-info/top_level.txt +2 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,6 @@
1
+ prune activity_ui/tests
2
+ prune src/agora_workbench/code_execution/tests
3
+ prune src/agora_workbench/connector/tests
4
+ prune src/agora_workbench/deployment/tests
5
+ global-exclude *.pyc
6
+ global-exclude *.pyo
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: agora-workbench
3
+ Version: 0.1.1
4
+ Summary: A toolkit for building MCP servers that provide sandboxed Python execution with domain-specific packages.
5
+ License-Expression: MIT
6
+ Project-URL: homepage, https://github.com/microsoft/agora-workbench
7
+ Project-URL: documentation, https://microsoft.github.io/agora-workbench
8
+ Project-URL: repository, https://github.com/microsoft/agora-workbench
9
+ Project-URL: changelog, https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: azure-core>=1.38.0
14
+ Requires-Dist: azure-data-tables>=12.7.0
15
+ Requires-Dist: azure-identity>=1.25.0
16
+ Requires-Dist: azure-search-documents>=11.4.0
17
+ Requires-Dist: azure-storage-blob[aio]>=12.28.0
18
+ Requires-Dist: fastapi>=0.115
19
+ Requires-Dist: fastmcp>=2.14.2
20
+ Requires-Dist: mcp>=1.28.1
21
+ Requires-Dist: starlette>=1.3.1
22
+ Requires-Dist: uvicorn>=0.34
23
+ Requires-Dist: wsproto>=1.3.2
24
+ Requires-Dist: cryptography>=48.0.1
25
+ Requires-Dist: msal>=1.28.0
26
+ Requires-Dist: pyjwt[crypto]>=2.8.0
27
+ Requires-Dist: pandas>=2.3.3
28
+ Requires-Dist: pyarrow>=22.0.0
29
+ Requires-Dist: pydantic>=2.10.4
30
+ Requires-Dist: pyyaml>=6.0
31
+ Requires-Dist: dill>=0.3.8
32
+ Requires-Dist: ipykernel>=6.29.0
33
+ Requires-Dist: jupyter-client>=8.6.0
34
+ Requires-Dist: pyzmq>=26.0.0
35
+ Requires-Dist: sqlite-vec>=0.1.9
36
+ Requires-Dist: httpx>=0.27.0
37
+ Requires-Dist: sse-starlette>=3.3.0
38
+ Requires-Dist: typeguard>=4.4.4
39
+ Requires-Dist: typing-extensions>=4.12.0
40
+ Provides-Extra: openai-agents
41
+ Requires-Dist: openai-agents>=0.15.0; extra == "openai-agents"
42
+ Provides-Extra: copilot-sdk
43
+ Requires-Dist: github-copilot-sdk>=1.0.0; extra == "copilot-sdk"
44
+ Requires-Dist: python-dotenv>=1.1.1; extra == "copilot-sdk"
45
+ Provides-Extra: geo
46
+ Requires-Dist: rasterio>=1.4.0; extra == "geo"
47
+ Requires-Dist: titiler.core>=0.19.0; extra == "geo"
48
+ Dynamic: license-file
49
+
50
+ # Agora Workbench
51
+ <p align="left">
52
+ <img src="logo.png" alt="Agora Workbench logo" width="250">
53
+ </p>
54
+
55
+
56
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
57
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
58
+
59
+ A workbench for wrapping your tooling with MCP
60
+
61
+ <h2><a href="https://microsoft.github.io/agora-workbench">Documentation</a></h2>
62
+
63
+ > [!IMPORTANT]
64
+ > **Research software and support**
65
+ >
66
+ > Agora Workbench is research software and is not an officially supported Microsoft product. It is provided
67
+ > as-is and may change without notice. No support, service-level commitments, or compatibility guarantees are
68
+ > provided. Issues and contributions are welcome, but responses, fixes, and continued maintenance are not
69
+ > guaranteed.
70
+
71
+ ## Overview
72
+
73
+ Agora Workbench is a toolkit for building MCP (Model Context Protocol) servers that provide sandboxed Python execution with domain-specific packages. It is agent-framework agnostic — any MCP-compatible client can take advantage of the servers created by Agora Workbench.
74
+
75
+ Use Agora Workbench to:
76
+
77
+ - **Wrap domain-specific Python tooling as MCP servers** — expose Python environments through isolated, session-aware execution environments that any MCP client can call
78
+ - **Make tools discoverable** — register tools and skills in a searchable catalog so agents can find what they need by natural-language query
79
+ - **Serve data alongside code** — attach a file catalog so agents can locate and load datasets without hardcoded paths
80
+ - **Deploy to Azure Container Apps** — use the included Bicep templates and CLI to ship your servers with Entra ID auth and managed identity
81
+
82
+ ## Getting Started
83
+
84
+ ### Prerequisites
85
+
86
+ - **Python 3.11+**
87
+ - **Git**
88
+ - **[uv](https://docs.astral.sh/uv/)** for dependency management
89
+ - **Docker** (required for the bundled container examples and local Compose workflows)
90
+
91
+ ### Installation
92
+
93
+ **With uv (recommended for development):**
94
+
95
+ ```bash
96
+ git clone https://github.com/microsoft/agora-workbench.git
97
+ cd agora-workbench
98
+ uv sync --group dev # install the project and dev tools
99
+ ```
100
+
101
+ **With pip (for using as a library):**
102
+
103
+ ```bash
104
+ pip install "agora-workbench==0.1.1"
105
+ ```
106
+
107
+ The installed deployment CLI can also scaffold the standalone Activity UI:
108
+
109
+ ```bash
110
+ agora-workbench-deploy init --target activity-ui
111
+ ```
112
+
113
+ With uv, add the same release to your project with:
114
+
115
+ ```bash
116
+ uv add "agora-workbench==0.1.1"
117
+ ```
118
+
119
+ See the [changelog](https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md) for release history and
120
+ [release guide](https://github.com/microsoft/agora-workbench/blob/main/RELEASING.md) for the versioning policy.
121
+
122
+ **Optional extras for examples** — the base package is all you need to build and run MCP servers. Extras pull in dependencies used by the example integrations:
123
+
124
+ | Extra | Example integration |
125
+ |-------|---------------------|
126
+ | `openai-agents` | OpenAI Agents SDK adapter |
127
+ | `copilot-sdk` | GitHub Copilot SDK adapter |
128
+ | `geo` | Geospatial example server dependencies (rasterio, etc.) |
129
+
130
+ Use the command for your installation method:
131
+
132
+ ```bash
133
+ # uv (inside the cloned repo)
134
+ uv sync --extra openai-agents
135
+
136
+ # pip (consuming as a library)
137
+ pip install "agora-workbench[openai-agents]==0.1.1"
138
+ ```
139
+
140
+ ### Configuration
141
+
142
+ For local testing, no external credentials are required. Use the no-op auth config to skip authentication entirely:
143
+
144
+ ```python
145
+ from agora_workbench.code_execution import CodeExecutionServer, ServerConfig
146
+ from agora_workbench.code_execution.auth import create_noop_auth_config
147
+
148
+ config = ServerConfig(
149
+ name="myserver",
150
+ description="My local test server",
151
+ type="uv",
152
+ dependency_file="numpy\npandas\n",
153
+ )
154
+ server = CodeExecutionServer(
155
+ server_config=config,
156
+ auth_config=create_noop_auth_config(),
157
+ )
158
+ ```
159
+ For Docker-based local deployment and Azure Container Apps, see the [deployment guide](https://microsoft.github.io/agora-workbench/guide/deploying/). For Entra ID authentication setup, see the [authentication guide](https://microsoft.github.io/agora-workbench/guide/authentication/).
160
+
161
+ ## Contributing
162
+
163
+ This project welcomes contributions and suggestions. Most contributions require you to agree to a
164
+ Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
165
+ the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com).
166
+
167
+ When you submit a pull request, a CLA bot will automatically determine whether you need to provide
168
+ a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
169
+ provided by the bot. You will only need to do this once across all repos using our CLA.
170
+
171
+ **Guidelines:**
172
+ - For changes more complex than typos, please submit an issue first to discuss the proposed changes
173
+ - Follow the development practices outlined in the project documentation
174
+
175
+ ### Contact
176
+
177
+ For questions or feedback, please [open an issue](https://github.com/microsoft/agora-workbench/issues).
178
+
179
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
180
+ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
181
+ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
182
+
183
+ ## Trademarks
184
+
185
+ This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
186
+ trademarks or logos is subject to and must follow
187
+ [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
188
+ Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
189
+ Any use of third-party trademarks or logos are subject to those third-party's policies.
@@ -0,0 +1,140 @@
1
+ # Agora Workbench
2
+ <p align="left">
3
+ <img src="logo.png" alt="Agora Workbench logo" width="250">
4
+ </p>
5
+
6
+
7
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
8
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
+
10
+ A workbench for wrapping your tooling with MCP
11
+
12
+ <h2><a href="https://microsoft.github.io/agora-workbench">Documentation</a></h2>
13
+
14
+ > [!IMPORTANT]
15
+ > **Research software and support**
16
+ >
17
+ > Agora Workbench is research software and is not an officially supported Microsoft product. It is provided
18
+ > as-is and may change without notice. No support, service-level commitments, or compatibility guarantees are
19
+ > provided. Issues and contributions are welcome, but responses, fixes, and continued maintenance are not
20
+ > guaranteed.
21
+
22
+ ## Overview
23
+
24
+ Agora Workbench is a toolkit for building MCP (Model Context Protocol) servers that provide sandboxed Python execution with domain-specific packages. It is agent-framework agnostic — any MCP-compatible client can take advantage of the servers created by Agora Workbench.
25
+
26
+ Use Agora Workbench to:
27
+
28
+ - **Wrap domain-specific Python tooling as MCP servers** — expose Python environments through isolated, session-aware execution environments that any MCP client can call
29
+ - **Make tools discoverable** — register tools and skills in a searchable catalog so agents can find what they need by natural-language query
30
+ - **Serve data alongside code** — attach a file catalog so agents can locate and load datasets without hardcoded paths
31
+ - **Deploy to Azure Container Apps** — use the included Bicep templates and CLI to ship your servers with Entra ID auth and managed identity
32
+
33
+ ## Getting Started
34
+
35
+ ### Prerequisites
36
+
37
+ - **Python 3.11+**
38
+ - **Git**
39
+ - **[uv](https://docs.astral.sh/uv/)** for dependency management
40
+ - **Docker** (required for the bundled container examples and local Compose workflows)
41
+
42
+ ### Installation
43
+
44
+ **With uv (recommended for development):**
45
+
46
+ ```bash
47
+ git clone https://github.com/microsoft/agora-workbench.git
48
+ cd agora-workbench
49
+ uv sync --group dev # install the project and dev tools
50
+ ```
51
+
52
+ **With pip (for using as a library):**
53
+
54
+ ```bash
55
+ pip install "agora-workbench==0.1.1"
56
+ ```
57
+
58
+ The installed deployment CLI can also scaffold the standalone Activity UI:
59
+
60
+ ```bash
61
+ agora-workbench-deploy init --target activity-ui
62
+ ```
63
+
64
+ With uv, add the same release to your project with:
65
+
66
+ ```bash
67
+ uv add "agora-workbench==0.1.1"
68
+ ```
69
+
70
+ See the [changelog](https://github.com/microsoft/agora-workbench/blob/main/CHANGELOG.md) for release history and
71
+ [release guide](https://github.com/microsoft/agora-workbench/blob/main/RELEASING.md) for the versioning policy.
72
+
73
+ **Optional extras for examples** — the base package is all you need to build and run MCP servers. Extras pull in dependencies used by the example integrations:
74
+
75
+ | Extra | Example integration |
76
+ |-------|---------------------|
77
+ | `openai-agents` | OpenAI Agents SDK adapter |
78
+ | `copilot-sdk` | GitHub Copilot SDK adapter |
79
+ | `geo` | Geospatial example server dependencies (rasterio, etc.) |
80
+
81
+ Use the command for your installation method:
82
+
83
+ ```bash
84
+ # uv (inside the cloned repo)
85
+ uv sync --extra openai-agents
86
+
87
+ # pip (consuming as a library)
88
+ pip install "agora-workbench[openai-agents]==0.1.1"
89
+ ```
90
+
91
+ ### Configuration
92
+
93
+ For local testing, no external credentials are required. Use the no-op auth config to skip authentication entirely:
94
+
95
+ ```python
96
+ from agora_workbench.code_execution import CodeExecutionServer, ServerConfig
97
+ from agora_workbench.code_execution.auth import create_noop_auth_config
98
+
99
+ config = ServerConfig(
100
+ name="myserver",
101
+ description="My local test server",
102
+ type="uv",
103
+ dependency_file="numpy\npandas\n",
104
+ )
105
+ server = CodeExecutionServer(
106
+ server_config=config,
107
+ auth_config=create_noop_auth_config(),
108
+ )
109
+ ```
110
+ For Docker-based local deployment and Azure Container Apps, see the [deployment guide](https://microsoft.github.io/agora-workbench/guide/deploying/). For Entra ID authentication setup, see the [authentication guide](https://microsoft.github.io/agora-workbench/guide/authentication/).
111
+
112
+ ## Contributing
113
+
114
+ This project welcomes contributions and suggestions. Most contributions require you to agree to a
115
+ Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
116
+ the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com).
117
+
118
+ When you submit a pull request, a CLA bot will automatically determine whether you need to provide
119
+ a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
120
+ provided by the bot. You will only need to do this once across all repos using our CLA.
121
+
122
+ **Guidelines:**
123
+ - For changes more complex than typos, please submit an issue first to discuss the proposed changes
124
+ - Follow the development practices outlined in the project documentation
125
+
126
+ ### Contact
127
+
128
+ For questions or feedback, please [open an issue](https://github.com/microsoft/agora-workbench/issues).
129
+
130
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
131
+ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
132
+ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
133
+
134
+ ## Trademarks
135
+
136
+ This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
137
+ trademarks or logos is subject to and must follow
138
+ [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
139
+ Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
140
+ Any use of third-party trademarks or logos are subject to those third-party's policies.
@@ -0,0 +1,21 @@
1
+ # Activity UI sidecar — receives events from MCP servers and serves the UI.
2
+ #
3
+ # Build from the directory containing activity_ui/:
4
+ # docker build -f activity_ui/Dockerfile -t activity-ui:local .
5
+
6
+ FROM mcr.microsoft.com/azurelinux/base/python:3.12
7
+
8
+ ENV PYTHONDONTWRITEBYTECODE=1 \
9
+ PYTHONUNBUFFERED=1 \
10
+ PIP_NO_CACHE_DIR=1
11
+
12
+ WORKDIR /app
13
+
14
+ COPY activity_ui/requirements.txt /app/requirements.txt
15
+ RUN pip3 install --no-input -r /app/requirements.txt
16
+
17
+ COPY activity_ui /app/activity_ui
18
+
19
+ EXPOSE 8030
20
+
21
+ CMD ["python3", "-m", "activity_ui.server"]
@@ -0,0 +1,5 @@
1
+ """Activity UI service — receives events from MCP servers and shows them to the user."""
2
+
3
+ from .models import ActivityEvent
4
+
5
+ __all__ = ["ActivityEvent"]
@@ -0,0 +1,298 @@
1
+ """App-layer authentication for Activity UI endpoints excluded from EasyAuth.
2
+
3
+ Follows the same architecture as ``code_execution.auth``:
4
+
5
+ - Abstract ``TokenValidator`` base class with async ``validate()`` method.
6
+ - ``EntraTokenValidator`` for production (PyJWT + JWKS, RS256, issuer/audience).
7
+ - ``NoOpTokenValidator`` for local development (accepts any token).
8
+
9
+ POST /events is excluded from EasyAuth to allow managed-identity bearer tokens.
10
+ The validator checks signature, issuer, audience, expiry, and the ``roles``
11
+ claim containing ``ActivityEventWriter``.
12
+
13
+ GET /stream and /events/recent use a short-lived stream token (HMAC-SHA256)
14
+ delivered as an HttpOnly cookie. The token is minted by POST /stream-token,
15
+ which is protected by EasyAuth (not in excludedPaths).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import logging
22
+ import os
23
+ import secrets
24
+ import time
25
+ from abc import ABC, abstractmethod
26
+ from datetime import datetime, timezone
27
+
28
+ import jwt
29
+ from fastapi import HTTPException, Request, Response
30
+ from jwt import PyJWKClient
31
+
32
+ LOGGER = logging.getLogger(__name__)
33
+
34
+ # Microsoft identity platform OIDC configuration
35
+ _ENTRA_JWKS_URL_TEMPLATE = "https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
36
+ _ENTRA_ISSUER_TEMPLATES = [
37
+ "https://login.microsoftonline.com/{tenant_id}/v2.0",
38
+ "https://sts.windows.net/{tenant_id}/",
39
+ ]
40
+
41
+ _REQUIRED_ROLE = "ActivityEventWriter"
42
+
43
+
44
+ # ── Abstract base ────────────────────────────────────────────────────────────
45
+
46
+
47
+ class TokenValidationError(Exception):
48
+ """Raised when token validation fails."""
49
+
50
+ def __init__(self, message: str, status_code: int = 401):
51
+ self.status_code = status_code
52
+ super().__init__(message)
53
+
54
+
55
+ class TokenValidator(ABC):
56
+ """Validates bearer tokens from incoming requests."""
57
+
58
+ @abstractmethod
59
+ async def validate(self, token: str) -> dict:
60
+ """Validate a bearer token and return decoded claims.
61
+
62
+ Raises:
63
+ TokenValidationError: If the token is invalid.
64
+ """
65
+ raise NotImplementedError("Subclasses must implement validate().")
66
+
67
+
68
+ # ── Entra ID implementation ──────────────────────────────────────────────────
69
+
70
+
71
+ class EntraTokenValidator(TokenValidator):
72
+ """Validates JWTs issued by Microsoft Entra ID using PyJWT.
73
+
74
+ Fetches signing keys from the JWKS endpoint, verifies signature (RS256),
75
+ and validates standard claims (exp, aud, iss) plus the ``roles`` claim.
76
+ """
77
+
78
+ def __init__(self, client_id: str, tenant_id: str, audience: str = ""):
79
+ self._client_id = client_id
80
+ self._tenant_id = tenant_id
81
+ jwks_url = _ENTRA_JWKS_URL_TEMPLATE.format(tenant_id=tenant_id)
82
+ self._jwks_client = PyJWKClient(jwks_url, cache_keys=True)
83
+ # Accept multiple audience values (scoped URI, bare client ID, api:// prefix)
84
+ self._valid_audiences: list[str] = []
85
+ if audience:
86
+ self._valid_audiences.append(audience)
87
+ if client_id:
88
+ self._valid_audiences.append(client_id)
89
+ self._valid_audiences.append(f"api://{client_id}")
90
+ self._valid_issuers = [t.format(tenant_id=tenant_id) for t in _ENTRA_ISSUER_TEMPLATES]
91
+
92
+ async def validate(self, token: str) -> dict:
93
+ try:
94
+ signing_key = await asyncio.to_thread(self._jwks_client.get_signing_key_from_jwt, token)
95
+ payload = jwt.decode(
96
+ token,
97
+ signing_key.key,
98
+ algorithms=["RS256"],
99
+ audience=self._valid_audiences,
100
+ issuer=self._valid_issuers,
101
+ options={"verify_exp": True, "verify_aud": True, "verify_iss": True},
102
+ )
103
+ except jwt.ExpiredSignatureError:
104
+ raise TokenValidationError("Token is expired", status_code=401)
105
+ except jwt.InvalidAudienceError:
106
+ raise TokenValidationError("Token has invalid audience", status_code=401)
107
+ except jwt.InvalidIssuerError:
108
+ raise TokenValidationError("Token has invalid issuer", status_code=401)
109
+ except jwt.PyJWKClientError as exc:
110
+ LOGGER.error("JWKS key fetch failed: %s", exc)
111
+ raise TokenValidationError(f"Failed to fetch signing keys: {exc}", status_code=401)
112
+ except jwt.InvalidTokenError as exc:
113
+ LOGGER.warning("Token validation failed: %s", exc)
114
+ raise TokenValidationError(str(exc), status_code=401)
115
+
116
+ # Check roles claim (normalize to list in case of unexpected type)
117
+ roles = payload.get("roles", [])
118
+ if not isinstance(roles, list):
119
+ roles = [roles] if isinstance(roles, str) else []
120
+ if _REQUIRED_ROLE not in roles:
121
+ LOGGER.debug("Token missing required role %r, has: %s", _REQUIRED_ROLE, roles)
122
+ raise TokenValidationError(f"Missing required role: {_REQUIRED_ROLE}", status_code=403)
123
+
124
+ return payload
125
+
126
+
127
+ # ── No-op implementation (local dev) ─────────────────────────────────────────
128
+
129
+
130
+ class NoOpTokenValidator(TokenValidator):
131
+ """Accepts any bearer token without validation.
132
+
133
+ WARNING: Never use in production. Exists for local development only.
134
+ """
135
+
136
+ async def validate(self, token: str) -> dict:
137
+ LOGGER.warning("NoOpTokenValidator: accepting token without validation (development mode)")
138
+ # Try to decode payload for claims (no signature check)
139
+ try:
140
+ return jwt.decode(token, options={"verify_signature": False}) if token else {}
141
+ except jwt.PyJWTError:
142
+ return {}
143
+
144
+
145
+ # ── Factory ──────────────────────────────────────────────────────────────────
146
+
147
+
148
+ def create_token_validator() -> TokenValidator:
149
+ """Create the appropriate TokenValidator based on environment configuration.
150
+
151
+ Returns NoOpTokenValidator when ACTIVITY_UI_AUTH_DISABLED=true.
152
+ Returns EntraTokenValidator when the tenant and either the audience or
153
+ client ID are configured.
154
+ Raises RuntimeError if auth is enabled but config is incomplete.
155
+ """
156
+ if os.getenv("ACTIVITY_UI_AUTH_DISABLED", "").lower() == "true":
157
+ LOGGER.warning("Activity UI auth DISABLED — using NoOpTokenValidator")
158
+ return NoOpTokenValidator()
159
+
160
+ tenant_id = os.getenv("ENTRA_TENANT_ID", "")
161
+ audience = os.getenv("ACTIVITY_UI_AUDIENCE", "")
162
+ client_id = os.getenv("ACTIVITY_UI_CLIENT_ID", "")
163
+
164
+ if not tenant_id or not (audience or client_id):
165
+ raise RuntimeError(
166
+ "Activity UI auth is enabled but not configured. "
167
+ "Set ENTRA_TENANT_ID and ACTIVITY_UI_AUDIENCE/ACTIVITY_UI_CLIENT_ID, "
168
+ "or set ACTIVITY_UI_AUTH_DISABLED=true for local development."
169
+ )
170
+
171
+ return EntraTokenValidator(client_id=client_id, tenant_id=tenant_id, audience=audience)
172
+
173
+
174
+ # ── FastAPI dependency ───────────────────────────────────────────────────────
175
+
176
+ # Singleton validator created at import time. In production this fails fast
177
+ # if auth config is missing; in local dev it returns NoOpTokenValidator.
178
+ _validator: TokenValidator | None = None
179
+
180
+
181
+ def _get_validator() -> TokenValidator:
182
+ global _validator
183
+ if _validator is None:
184
+ _validator = create_token_validator()
185
+ return _validator
186
+
187
+
188
+ async def require_event_writer(request: Request) -> None:
189
+ """FastAPI dependency that validates bearer tokens on /events.
190
+
191
+ Raises HTTPException(401/403) on failure.
192
+ """
193
+ validator = _get_validator()
194
+
195
+ # Extract bearer token
196
+ auth_header = request.headers.get("Authorization", "")
197
+ if not auth_header.startswith("Bearer "):
198
+ if isinstance(validator, NoOpTokenValidator):
199
+ return
200
+ raise HTTPException(status_code=401, detail="Missing bearer token")
201
+
202
+ token = auth_header[7:]
203
+
204
+ try:
205
+ await validator.validate(token)
206
+ except TokenValidationError as exc:
207
+ raise HTTPException(status_code=exc.status_code, detail=str(exc))
208
+
209
+
210
+ # ── Stream token (browser access to /stream and /events/recent) ──────────────
211
+
212
+ # Server-generated secret for HMAC-signed stream tokens.
213
+ # Rotates on restart — acceptable for short-lived tokens on single-replica.
214
+ _STREAM_TOKEN_SECRET: str = os.getenv("ACTIVITY_UI_STREAM_SECRET", "") or secrets.token_hex(32)
215
+ _STREAM_TOKEN_TTL_SECONDS = int(os.getenv("ACTIVITY_UI_STREAM_TOKEN_TTL", "300")) # 5 min default
216
+ _STREAM_TOKEN_COOKIE = "activity_stream_token"
217
+ _STREAM_TOKEN_ALGORITHM = "HS256"
218
+
219
+
220
+ def mint_stream_token(subject: str) -> str:
221
+ """Create a short-lived HMAC stream token for browser SSE access."""
222
+ now = time.time()
223
+ payload = {
224
+ "sub": subject,
225
+ "purpose": "stream",
226
+ "iat": int(now),
227
+ "exp": int(now) + _STREAM_TOKEN_TTL_SECONDS,
228
+ }
229
+ return jwt.encode(payload, _STREAM_TOKEN_SECRET, algorithm=_STREAM_TOKEN_ALGORITHM)
230
+
231
+
232
+ def validate_stream_token(token: str) -> dict:
233
+ """Validate a stream token. Returns claims or raises TokenValidationError."""
234
+ try:
235
+ claims = jwt.decode(
236
+ token, _STREAM_TOKEN_SECRET, algorithms=[_STREAM_TOKEN_ALGORITHM], options={"require": ["exp", "iat"]}
237
+ )
238
+ except jwt.ExpiredSignatureError:
239
+ raise TokenValidationError("Stream token expired", status_code=401)
240
+ except jwt.MissingRequiredClaimError as exc:
241
+ raise TokenValidationError(f"Stream token missing required claim: {exc}", status_code=401)
242
+ except jwt.InvalidTokenError as exc:
243
+ raise TokenValidationError(f"Invalid stream token: {exc}", status_code=401)
244
+
245
+ if claims.get("purpose") != "stream":
246
+ raise TokenValidationError("Token not valid for stream access", status_code=403)
247
+
248
+ return claims
249
+
250
+
251
+ def stream_token_expiry(token: str) -> datetime | None:
252
+ """Return the expiry time of a stream token (without full validation)."""
253
+ try:
254
+ claims = jwt.decode(
255
+ token, _STREAM_TOKEN_SECRET, algorithms=[_STREAM_TOKEN_ALGORITHM], options={"verify_exp": False}
256
+ )
257
+ exp = claims.get("exp")
258
+ return datetime.fromtimestamp(exp, tz=timezone.utc) if exp else None
259
+ except jwt.InvalidTokenError:
260
+ return None
261
+
262
+
263
+ def set_stream_token_cookie(response: Response, token: str) -> None:
264
+ """Set the stream token as an HttpOnly, Secure, SameSite=Lax cookie."""
265
+ response.set_cookie(
266
+ key=_STREAM_TOKEN_COOKIE,
267
+ value=token,
268
+ httponly=True,
269
+ secure=True,
270
+ samesite="lax",
271
+ max_age=_STREAM_TOKEN_TTL_SECONDS,
272
+ path="/",
273
+ )
274
+
275
+
276
+ async def require_stream_reader(request: Request) -> dict:
277
+ """FastAPI dependency that validates stream tokens on /stream and /events/recent.
278
+
279
+ Checks the stream token cookie (HttpOnly, set by POST /stream-token).
280
+ Falls back to query param for non-browser clients (e.g. curl for debugging).
281
+ Returns validated claims dict.
282
+
283
+ In NoOp mode (local dev), all requests are allowed.
284
+ """
285
+ validator = _get_validator()
286
+ if isinstance(validator, NoOpTokenValidator):
287
+ return {}
288
+
289
+ # Check cookie first, then query param fallback
290
+ token = request.cookies.get(_STREAM_TOKEN_COOKIE) or request.query_params.get("token")
291
+
292
+ if not token:
293
+ raise HTTPException(status_code=401, detail="Missing stream token")
294
+
295
+ try:
296
+ return validate_stream_token(token)
297
+ except TokenValidationError as exc:
298
+ raise HTTPException(status_code=exc.status_code, detail=str(exc))