Skip to content

Support hosting the Teams endpoint inside a caller-owned ASGI app (sync route registration, host owns lifecycle) #447

Description

@eavanvalkenburg

Summary

Please provide a first-class, supported way to mount the Teams messaging endpoint onto an externally-owned ASGI app, where the host owns the server lifecycle and route registration is available synchronously (decoupled from async plugin initialization).

Today the SDK is designed around the App owning (or at least driving) the HTTP server through an HttpServerAdapter. There is a "bring your own Starlette" pattern in examples/http-adapters/src/starlette_adapter.py, but it only works by calling the async App.initialize() to register routes, and the example adapter is not shipped in the package. That makes clean integration into a host that already owns the ASGI app, the port, and the startup/shutdown lifecycle surprisingly hard.

Motivation / use case

We are building a multi-channel agent host (the Microsoft Agent Framework) where a single ASGI/Starlette application is owned by the host and serves many channels behind one public URL (Bot Service, Telegram, Discord, A2A, MCP, ...). Each channel contributes routes + startup/shutdown hooks; the host mounts them all and owns binding/serving.

To embed Teams there we want to:

  1. Reuse the SDK's JWT validation, activity parsing, dispatch, and streaming (we don't want to reimplement Bot Framework auth).
  2. Not let the SDK own the server — the host binds the port and runs uvicorn.
  3. Obtain the registered routes synchronously at "contribution" time (before the event loop starts serving), because the host needs the route table up front, while async plugin on_init work must run later at startup.

What we had to do today (and why it's painful)

Because route registration currently happens inside HttpServer.initialize(...) which is only invoked from the async App.initialize(), and because the only Starlette adapter is an example (not importable), we ended up:

  • Writing a custom capturing HttpServerAdapter whose register_route just records (method, path) -> handler, and whose serve_static / start / stop are no-ops (host owns those).
  • Calling the private app.server.initialize(...) ourselves to force synchronous route registration.
  • Manually wiring app.server.on_request = app._process_activity_event (private) because that wiring normally happens inside App.initialize(), which we deliberately skip at route-collection time.
  • Re-implementing the Starlette↔HttpRequest/HttpResponse translation, then calling the async App.initialize() later (at host startup) purely to run plugin on_init hooks — guarding against the SDK's private _initialized re-running the sync step.

This works but reaches into private internals (server._adapter, server.initialize, _process_activity_event, _initialized) and is fragile against minor SDK changes.

Proposed API (illustrative)

A public, lifecycle-free integration seam. For example, any of:

  • app.register_routes() -> list[tuple[HttpMethod, str, HttpRouteHandler]] — synchronously perform auth setup + route registration and return the route table, without owning a server and without running async plugin init.
  • Split App.initialize() so that the synchronous "register routes / wire dispatcher" step is a separate, public, sync-callable method from the async plugin on_init step (e.g. app.register_routes() (sync) + await app.start_plugins() (async)).
  • A shipped (not example-only) ASGI/Starlette adapter that supports "bring your own app, host owns lifecycle", with route registration callable without await app.start() and clearly separated from plugin init.

Concretely, the host wants something like:

app = App(client_id=..., client_secret=..., messaging_endpoint="/teams/messages")
routes = app.register_routes()        # sync: auth + messaging endpoint, NO server, NO async plugin init
host.mount(routes)                    # host owns the ASGI app + port
# ... at host startup:
await app.start_plugins()             # async: plugin on_init only; never binds a port

Acceptance criteria

  • A documented, non-private way to obtain/register the Teams messaging route(s) onto a caller-owned ASGI app.
  • Route registration callable synchronously (or at least separable from the async plugin-init step), so a host can build its route table before serving.
  • The SDK never tries to bind a port / own start-stop in this mode.
  • A shipped Starlette (and/or FastAPI) "bring your own app" adapter, so consumers don't copy the examples/ adapter.

Happy to contribute a PR if the team is open to one of these shapes.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions