Convert to remote HTTP MCP server with Auth0 + Key Vault#23
Draft
searledan wants to merge 4 commits into
Draft
Conversation
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>
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
ModelContextProtocol.AspNetCore/.well-known/oauth-protected-resourceper RFC 9728VitallyApiKeyProviderreads thesecret_refclaim from the user's JWT, fetches the named secret from Azure Key Vault (5-min in-memory cache), falls back toDefaultSecretRefVitallyConfigsplit into singletonVitallyServerOptions+ scopedVitallyApiKeyProvider;VitallyServiceis scoped,HttpClientis never mutatedAuth0:NoAuth=true+Vitally:DevelopmentApiKeyRemoved:
Check_for_updatestool,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 warningsdotnet test— 181/181 passdotnet run(withNoAuth=true) — server starts on:5099GET /.well-known/oauth-protected-resource— metadata document respondsPOST /mcpinitialize— returns protocol 2025-06-18 + tools capabilityPOST /mcptools/list— returns 92 toolsWhy 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 tomain.Test plan
fiscal-it; Vitally key uploaded to KV asvitally-sharedclaude mcp add --transport http vitally https://vitally-mcp.fiscaltec.com/mcp→ OAuth flow completes →List_accountsreturns real Vitally datamain🤖 Generated with Claude Code