Skip to content

Convert to remote HTTP MCP server with Auth0 + Key Vault#23

Draft
searledan wants to merge 4 commits into
mainfrom
feat/remote-http-mcp-server
Draft

Convert to remote HTTP MCP server with Auth0 + Key Vault#23
searledan wants to merge 4 commits into
mainfrom
feat/remote-http-mcp-server

Conversation

@searledan
Copy link
Copy Markdown
Collaborator

Summary

BREAKING change. Converts the Vitally MCP server from a stdio + MCPB binary distribution into a hosted ASP.NET Core HTTP MCP server. Users connect by URL — no install, no executable to distribute, instant updates.

  • Streamable HTTP transport (MCP 2025-06-18, stateless) on ModelContextProtocol.AspNetCore
  • Auth0 OAuth 2.1 protection via JwtBearer; publishes /.well-known/oauth-protected-resource per RFC 9728
  • Per-request Vitally API key resolution: VitallyApiKeyProvider reads the secret_ref claim from the user's JWT, fetches the named secret from Azure Key Vault (5-min in-memory cache), falls back to DefaultSecretRef
  • VitallyConfig split into singleton VitallyServerOptions + scoped VitallyApiKeyProvider; VitallyService is scoped, HttpClient is never mutated
  • Local dev: Auth0:NoAuth=true + Vitally:DevelopmentApiKey

Removed: Check_for_updates tool, UpdateCheckService, VitallyConfig, Output/mcpb/, Scripts/build-*.ps1, .github/workflows/release.yml (container release lands in Phase 4).

Docs updated: README rewritten around connect-by-URL with a self-host section; CLAUDE.md architecture + dev-command sections updated; CHANGELOG v4.0.0 entry.

Verification

Local end-to-end:

  • dotnet build — clean, 0 warnings
  • dotnet test — 181/181 pass
  • dotnet run (with NoAuth=true) — server starts on :5099
  • GET /.well-known/oauth-protected-resource — metadata document responds
  • POST /mcp initialize — returns protocol 2025-06-18 + tools capability
  • POST /mcp tools/list — returns 92 tools

Why draft

This is the foundation. Phases 2 (Auth0 Resource Server + post-login Action in fiscal-it) and 3 (Azure Container Apps + ACR + Key Vault) will land as further commits on this branch. We deploy from this branch, prove it end-to-end with real auth + Vitally calls, then merge to main.

Test plan

  • CI green on this PR
  • Phase 2: Auth0 Resource Server + post-login Action created in fiscal-it; Vitally key uploaded to KV as vitally-shared
  • Phase 3: ACR + Container App + custom domain + managed identity provisioned; image pushed; CA revision active
  • Phase 4: claude mcp add --transport http vitally https://vitally-mcp.fiscaltec.com/mcp → OAuth flow completes → List_accounts returns real Vitally data
  • Drop draft + merge to main

🤖 Generated with Claude Code

BREAKING CHANGE: replaces stdio + MCPB binary distribution with a
hosted ASP.NET Core MCP server. Users connect by URL — no install.

Code
- Streamable HTTP transport (MCP 2025-06-18, stateless) on the
  ModelContextProtocol.AspNetCore package
- JwtBearer auth against Auth0 (fiscal-it tenant) with
  /.well-known/oauth-protected-resource published per RFC 9728
- Per-request Vitally API key resolution via new VitallyApiKeyProvider:
  reads the secret_ref claim, fetches the named secret from Azure Key
  Vault, caches in-memory for 5 min, falls back to DefaultSecretRef
- VitallyConfig split into singleton VitallyServerOptions (region,
  KV URI, claim config) plus scoped VitallyApiKeyProvider; VitallyService
  is scoped, HttpClient is never mutated, auth header set per request
- Local dev mode: Auth0:NoAuth=true + Vitally:DevelopmentApiKey

Removed
- Check_for_updates tool + UpdateCheckService (hosted server is the
  source of truth, no per-client version probe needed)
- VitallyConfig (replaced by VitallyServerOptions + VitallyApiKeyProvider)
- Output/mcpb/, Scripts/build-*.ps1, bump-version.ps1
- .github/workflows/release.yml (container release lands in Phase 4)

Docs
- README rewritten around "connect by URL" + self-host section for
  replicators
- CLAUDE.md architecture and dev-command sections rewritten
- CHANGELOG v4.0.0 entry documenting the breaking distribution change
- .gitignore simplified (MCPB/exe rules dropped)

Verified locally: 181/181 tests pass; server starts on :5099, the
OAuth-protected-resource metadata endpoint responds, MCP initialize
succeeds, tools/list returns 92 tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@searledan searledan self-assigned this May 18, 2026
searledan and others added 3 commits May 19, 2026 09:42
- Auth0 audience + claim namespace + server defaults switched from
  vitally-mcp.fiscaltec.com to vitally.fiscaltec.com to match the
  desired shorter subdomain
- Dockerfile (multi-stage: SDK build → chiseled ASPNET runtime, port
  8080) + .dockerignore for ACR Tasks server-side builds

Live in Azure on a placeholder host — managed cert binding in flight
on the vitally.fiscaltec.com Cloudflare CNAME.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: Auth0 is no longer in the auth path for this MCP.
The server now validates Entra-issued JWTs directly. Resolves the
RFC 8707 resource-indicator forwarding incompatibility we hit between
Claude Code (MCP 2025-06-18 spec) and the Auth0 → Entra federation
hop (AADSTS9010010 / 70011).

Architecture
- New Entra single-tenant App Registration "Vitally MCP" with
  identifierUri api://vitally.fiscaltec.com, delegated scope
  Tools.Access, and http://localhost public-client redirect for
  DCR-style MCP OAuth clients.
- JwtBearer authority now points at Entra v2 issuer
  (https://login.microsoftonline.com/{tenant-id}/v2.0); audience is
  the Entra app's identifier URI.
- VitallyApiKeyProvider simplified: drop the secret_ref claim
  resolution and always fetch the configured DefaultSecretRef from
  Key Vault. Per-user keys can be re-added later via Entra
  group/role claims; current FISCAL setup uses one shared
  service-account key.

Code
- Auth0Options.cs → EntraOptions.cs (section Auth0: → Entra:)
- Program.cs binds EntraOptions, configures JwtBearer accordingly,
  publishes the Entra issuer in /.well-known/oauth-protected-resource
- VitallyServerOptions.cs drops the SecretRefClaim property
- VitallyApiKeyProvider.cs drops IHttpContextAccessor and the
  ResolveSecretRef helper

Docs
- README + CLAUDE.md updated to describe Microsoft Entra direct auth
- appsettings.Example.json moves Auth0: → Entra:

Retired (cleanup pending in dashboards)
- Auth0 Resource Server "Vitally MCP" renamed to "(retired)"
- Auth0 Action "Vitally MCP claims" renamed to "(retired)" and
  reduced to a no-op
- FISCAL IT Auth0 Entra app's identifierUris reverted to []
- DCR-registered "Claude Code (vitally)" clients (six entries) —
  delete manually in Auth0 dashboard

Verified locally: 181/181 tests pass; build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dation)

Claude Code's SDK rejected our well-known metadata because the
`resource` field was `api://vitally.fiscaltec.com` while the client
expected the server's URL or origin (https://vitally.fiscaltec.com).
This is a sensible security check (RFC 9728): the resource identifier
the client is told to request should match the server it's talking to.

We were using one config value for two different purposes:
  - The `resource` field in /.well-known/oauth-protected-resource
    (must match the server URL/origin)
  - The JWT audience to validate against (whatever Entra puts in `aud`,
    which is the appId GUID for v2 tokens)

Splitting them:
  - EntraOptions.Resource → published in metadata, defaults to
    Audience for backward compat
  - EntraOptions.Audience → still used by JwtBearer
  - Production config now sets Resource=https://vitally.fiscaltec.com
    and Audience to the Vitally MCP Entra app's appId GUID

Also adds https://vitally.fiscaltec.com to the Vitally MCP Entra app's
identifierUris (alongside api://vitally.fiscaltec.com) so the OAuth
authorize flow accepts the resource indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant