Orpheus Systems Architecture

15. Dev tunnel for the agent demo

15. Dev tunnel for the agent demo

Date: 2026-10-22

Status

Accepted


Problem

The demo runs on the developer tenant, and the owner decided that nothing is deployed to Azure in this version (spec O5). The MCP server runs in Podman on the developer's machine on port 8787. Copilot calls plugins from Microsoft's cloud and needs a public HTTPS URL; it cannot reach localhost. The URL is baked into the agent package (MCP_SERVER_URL), so a URL that changes on every restart means re-provisioning the agent each time.


Decision

Expose the local server through a persistent Microsoft dev tunnel:

devtunnel user login
devtunnel create <tunnel-id> --allow-anonymous
devtunnel port create <tunnel-id> -p 8787 --protocol http
devtunnel host <tunnel-id>

A persistent tunnel keeps its host name across restarts, so the agent is provisioned once with MCP_SERVER_URL=https://<tunnel-host>/mcp. On the first run the "Connect via browser" URL is opened once and confirmed, as the Microsoft 365 debugging guide describes (debug MCP and API plugins locally).

Anonymous access is set on the tunnel because Copilot is not a dev tunnels client and cannot sign in to the relay. That access is about the relay only. The server still runs with AUTH_MODE=entra: every request without a valid token for its audience gets 401, and createApp refuses to start in entra mode without the auth middleware. AUTH_MODE=none is allowed only with DATA_MODE=local (fixtures, no SharePoint) and is used with MCP Inspector, never through the tunnel.

The devtunnel CLI runs on the host OS; the server runs in the mcp compose service (default compose project of the main checkout), published on host port 8787.

Local flow through the dev tunnel

flowchart LR
  CP["Copilot Chat<br/>sideloaded agent"] -->|"HTTPS, Entra SSO token"| DT["Dev tunnel relay<br/>persistent tunnel id<br/>anonymous at the relay"]
  subgraph PC["Developer machine"]
    HOST["devtunnel host, on the host OS"] --> PORT["localhost:8787"]
    PORT --> MCP["Podman service mcp<br/>MCP server, AUTH_MODE=entra"]
    MCP --> LOG["Audit JSON lines<br/>stdout or AUDIT_LOG_PATH"]
  end
  DT --> HOST
  MCP -->|"OBO token, SharePoint REST"| SP[("Dev tenant site")]
  INS["MCP Inspector"] -.->|"local checks, AUTH_MODE=none, DATA_MODE=local"| PORT

Considered Options

Option Summary Pros Cons
✅ Persistent dev tunnel devtunnel create once, devtunnel host per session Microsoft's tool, used by the Agents Toolkit and the plugin debugging docs; stable host name, so the agent is provisioned once; free; no Azure Public URL while hosted; up only while the developer machine is on
Temporary dev tunnel (devtunnel host -p 8787) New tunnel each run No setup Host name changes every run, so MCP_SERVER_URL and the agent package change every run (CLI reference)
ngrok or Cloudflare Tunnel Third-party tunnels Work the same way Another account, tool and data processor in the request path, outside the Microsoft tooling the Agents Toolkit already uses
Deploy to Azure now The target in 0016-azure-target-apim-container-apps-terraform.md Production-like Owner decided against it for this version; subscription question still open
Tunnel access restricted to the tenant (--tenant) Relay requires Entra sign-in Tighter Copilot cannot present a dev tunnels token, so it cannot reach the server

Consequences

Positive:

Negative / Trade-offs:

Why not for clients: a tunnel is a developer convenience with no SLA, no gateway, no rate limit, no managed secret store and no central log. The OBO client secret sits in a local .env. Audit events go to stdout or a local file. A client deployment uses the Azure target, where API Management, Key Vault and Log Analytics cover each of these gaps.

Risks: | Risk ID | Description | Impact (E/H/M/L) | Mitigation | |---------|-------------|------------------|------------| | R-AGT-12 | Tunnel left hosted and URL shared or guessed | M | Entra validation in the server; stop hosting after the session; delete the tunnel at the end | | R-AGT-13 | AUTH_MODE=none combined with a tunnel by mistake | H | Config rejects AUTH_MODE=none with DATA_MODE=sharepoint; with local data only fixture data is exposed; the demo procedure starts the tunnel only after an entra start |