An Azure-native remote MCP server built on ASP.NET Core, Azure Container Apps, and Cosmos DB. DevBrain gives AI tools persistent, shared access to developer knowledge across projects and clients.
DevBrain 2.0 uses the official Model Context Protocol (MCP) C# SDK and implements the 2026-07-28 revision of the MCP specification. The protocol revision is intentionally pinned here: informal references to “MCP v2” can otherwise be confused with the C# SDK’s own 2.x package version.
Every AI tool starts from zero. You paste the same sprint doc into Claude, copy architecture notes into Copilot, re-explain project state to Cursor. Each tool is an island. Your knowledge lives in scattered markdown files, and every conversation begins with a 6,000-character upload ritual.
DevBrain eliminates this. Deploy once, point any MCP client at the endpoint, and every AI tool you use shares the same persistent knowledge store.
One instance. Every project. Any AI tool.
Deploy DevBrain once and every project you work on shares the same knowledge store. Load context from multiple projects in a single session — no workspace switching, no separate deployments, no file uploads.
# Morning session — three projects, three tool calls
GetDocument(key="acme-platform:state:current", project="acme-platform")
GetDocument(key="devbrain:state:current", project="devbrain")
GetDocument(key="client-abc:state:current", project="client-abc")
Compare that to alternatives:
- Serena — per-repo MCP server, requires workspace switching between projects
- Claude Project Knowledge — manual file uploads, single project scope, resets between sessions
- Local markdown files — not shared across AI tools, no persistence
DevBrain is the only approach that gives every AI tool (Claude, Copilot, Codex, Cursor) shared persistent access across all your projects from a single deployed endpoint.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Claude Code CLI │ │ Claude Desktop │ │ Codex / Others │
└─────────┬────────┘ └─────────┬────────┘ └─────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│ MCP (Streamable HTTP + OAuth 2.0)
┌────────▼─────────┐
│ Container Apps │ ← ASP.NET Core + MCP SDK
│ (DevBrain 2.0) │ OAuth facade (Entra-backed)
└────────┬─────────┘
│ Managed Identity
┌────────▼─────────┐
│ Cosmos DB │
│ (NoSQL) │
└──────────────────┘
The MCP transport is stateless, so requests do not require session affinity or a distributed protocol-state cache. The template therefore does not provision Redis. It exposes a separate anonymous /healthz process/readiness endpoint rather than treating MCP JSON-RPC traffic as a health probe.
| Setting | Default |
|---|---|
| Container Apps replicas | Minimum 0, maximum 3 |
| Container resources | 0.5 vCPU, 1 GiB memory |
| Public endpoint rate limit | 120 requests per 60 seconds per replica and authenticated object ID (IP fallback) |
| Request body limit | 4 MiB |
| CORS | Disabled; configure explicit origins only when a browser client requires them |
| Public edge | Native Container Apps HTTPS FQDN; no Front Door dependency (optionally an existing Front Door profile with a custom domain and WAF) |
Minimum replicas are a latency/cost choice. This repository defaults to zero; latency-sensitive interactive deployments should consider one or more warm replicas, consistent with Microsoft’s stateless MCP hosting guidance.
- An Azure subscription where you can create resources and role assignments (Owner, or Contributor + User Access Administrator)
- Permission to create an app registration in the Entra tenant whose users will sign in (Application Developer or higher), and to assign users to its enterprise application
- Azure Developer CLI (
azd) - Azure CLI (
az), used for the Entra and verification commands below - PowerShell 7 (
pwsh), which runs the cross-platform post-provision hook - .NET 10 SDK
Docker isn't required locally, because azd builds the container image remotely in Azure Container Registry.
DevBrain is single-tenant: one deployment serves one Entra tenant. These steps take a fresh tenant from nothing to a working MCP endpoint.
DevBrain signs users in through one pre-registered Entra app. It asks Entra only for openid profile offline_access, so the app doesn't need an exposed API scope or extra API permissions.
Portal: Entra ID → App registrations → New registration
| Setting | Value |
|---|---|
| Name | DevBrain (any name works) |
| Supported account types | Accounts in this organizational directory only (single tenant). Don't pick a multi-tenant option. |
| Redirect URI | Leave blank for now. You'll add it in step 4, after the Container App host name exists. |
Then, on the new registration:
- Copy the Application (client) ID and Directory (tenant) ID from the Overview page.
- Certificates & secrets → New client secret. Copy the secret Value now; Entra shows it only once. Note the expiry date (see Operating notes).
- App roles → Create app role: display name
DevBrain User, allowed member types Users/Groups, valueDevBrain.User, enabled./mcprejects any caller without this role. - If your tenant blocks user consent, open API permissions and select Grant admin consent. Otherwise, users consent to the basic sign-in scopes on first login.
Azure CLI equivalent:
az login --tenant <tenant-guid>
$app = az ad app create --display-name DevBrain --sign-in-audience AzureADMyOrg | ConvertFrom-Json
az ad sp create --id $app.appId | Out-Null # creates the enterprise application used for role assignment
$clientSecret = az ad app credential reset --id $app.appId --display-name devbrain --years 1 --query password -o tsv
$roles = @(@{
allowedMemberTypes = @('User'); isEnabled = $true; value = 'DevBrain.User'
displayName = 'DevBrain User'; description = 'Can connect to DevBrain'; id = [guid]::NewGuid().ToString()
}) | ConvertTo-Json -Depth 5 -AsArray
Set-Content -Path approles.json -Value $roles
az ad app update --id $app.appId --app-roles approles.json
Remove-Item approles.json
$app.appId # this is ENTRA_CLIENT_IDEntra ID → Enterprise applications → DevBrain:
- Users and groups → Add user/group → select the people or security groups (group assignment requires Entra ID P1 or higher) → role DevBrain User.
- Recommended: Properties → set Assignment required? to Yes. Entra then stops unassigned users at sign-in, instead of letting them sign in and get rejected by DevBrain.
Role changes take effect the next time a client's short-lived access token refreshes, not immediately.
azd init -t Ignite-Solutions-Group/devbrain
azd auth login --tenant-id <tenant-guid>
azd env new <env-name> # e.g. devbrain-prod; drives resource naming
azd env set ENTRA_TENANT_ID <tenant-guid>
azd env set ENTRA_CLIENT_ID <app-client-id>
azd env set ENTRA_CLIENT_SECRET <client-secret-value>
azd env set JWT_SIGNING_SECRET ([Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)))
azd provisionazd provision creates the Container App and its environment, Azure Container Registry, Cosmos DB, Storage, Key Vault, and Log Analytics with Application Insights. It also creates the managed-identity role assignments. The two secret values seed Key Vault through secure deployment parameters, so they never appear in plain deployment history.
The post-provision hook finalizes the Container App's Cosmos DB data-role assignment. It retries while a newly created managed identity propagates through Entra, so the first deployment doesn't need a manual second provision. It's safe to rerun.
ENTRA_TENANT_ID and ENTRA_CLIENT_ID stay in the azd environment, and every later azd provision or azd up re-applies them. After the first successful provision, you can clear the two bootstrap secrets from the local environment. Later provisions leave the existing Key Vault secrets unchanged:
azd env set ENTRA_CLIENT_SECRET ""
azd env set JWT_SIGNING_SECRET ""azd env get-value OAUTH_REDIRECT_URI # https://ca-devbrain-<token>.<region>.azurecontainerapps.io/callback, or https://<CUSTOM_DOMAIN_NAME>/callbackIn the portal, open the app registration → Authentication → Add a platform → Web, and paste that value exactly. Or use the CLI:
az ad app update --id <app-client-id> --web-redirect-uris (azd env get-value OAUTH_REDIRECT_URI)azd deployAfter the first deployment, azd up (provision + deploy) is fine for later updates.
$url = azd env get-value DEVBRAIN_PUBLIC_URL
Invoke-RestMethod "$url/healthz" # status: healthy
Invoke-RestMethod "$url/.well-known/oauth-authorization-server" # DevBrain's own issuer metadataThen connect a client (see Configure Your MCP Client) at $url/mcp (also available as azd env get-value DEVBRAIN_MCP_URL). The first tool call opens an Entra sign-in, and every write records the signed-in user's UPN in updatedBy.
By default the Container App uses the platform-provided HTTPS host name, and DEVBRAIN_PUBLIC_URL is that host name. A custom domain and Azure Front Door are optional; see the next section.
DevBrain can publish through an Azure Front Door Standard or Premium profile you already have. The template adds a dedicated endpoint, route, origin, custom domain with a managed certificate, and a WAF policy to that profile. It doesn't change the profile's existing routes, WAF policies, or security policies. Set these values before azd provision, ideally before any client connects, because changing the public URL makes every client sign in again:
azd env set CUSTOM_DOMAIN_NAME devbrain.contoso.com
azd env set FRONT_DOOR_PROFILE_NAME <front-door-profile>
azd env set FRONT_DOOR_RESOURCE_GROUP <front-door-resource-group>
azd env set FRONT_DOOR_SUBSCRIPTION_ID <subscription-id> # only when the profile is in another subscription
azd env set DNS_ZONE_NAME contoso.com # optional: Azure DNS zone that hosts the domain
azd env set DNS_ZONE_RESOURCE_GROUP <dns-zone-resource-group>
azd env set DNS_ZONE_SUBSCRIPTION_ID <subscription-id> # only when the zone is in another subscription
azd env set FRONT_DOOR_ALLOWED_COUNTRIES US # optional: comma-separated country allow list
azd env set FRONT_DOOR_SKU Premium_AzureFrontDoor # only for a Premium profile
azd provisionThe Front Door profile and DNS zone can be in other subscriptions of the same tenant. The Front Door resource group and subscription default to DevBrain's own, and the DNS zone's default to the Front Door profile's. The identity that runs azd provision needs to be able to deploy into each of those resource groups.
CUSTOM_DOMAIN_NAME becomes the OAuth issuer, the protected-resource URL, and the redirect URI, so register the new OAUTH_REDIRECT_URI on the Entra app (step 4). With DNS_ZONE_NAME set, provisioning creates the CNAME and the _dnsauth validation TXT record. Without it, create both records yourself, using the FRONT_DOOR_ENDPOINT_HOST_NAME and FRONT_DOOR_DOMAIN_VALIDATION_TOKEN outputs. Front Door issues the managed certificate after it validates the domain, which can take several minutes.
To move an existing deployment behind Front Door, add the new redirect URI to the Entra app before you provision, and keep the old one until every client has moved. As soon as provisioning finishes, the Container Apps URL rejects MCP traffic, so point each client at DEVBRAIN_MCP_URL and sign in again. Remove the old redirect URI once nothing uses it.
The WAF policy uses custom rules only, so it works on the Standard tier:
| Rule | Behavior |
|---|---|
| Country allow list | Blocks clients outside FRONT_DOOR_ALLOWED_COUNTRIES when set |
| Path allow list | Blocks anything other than /mcp, /.well-known/*, /register, /authorize, /callback, /token, and /healthz |
| OAuth rate limit | 30 requests per minute per client IP to /register, /authorize, /callback, and /token |
| Overall rate limit | 1000 requests per five minutes per client IP |
With Front Door enabled, the app also sets FrontDoor__Id to the profile's ID and rejects any request without the matching X-Azure-FDID header. As a result, the *.azurecontainerapps.io host name can't be used to bypass the WAF. /healthz stays open for Container Apps health probes.
Front Door returns 504 when the origin doesn't respond within the profile's origin response timeout. With the default of zero minimum replicas, the first request after an idle period waits for a container to start, which can take 30 seconds or more. If the profile's timeout is close to or shorter than that, the first call can fail while later calls succeed. Set the timeout to at least 60 seconds (the allowed range is 16–240), or set containerAppMinReplicas to 1 in infra/main.bicep. The timeout is a profile-wide setting that also applies to the profile's other sites, and the template doesn't change it. To check the current value and raise it:
az afd profile show --subscription <subscription-id> --resource-group <front-door-resource-group> --profile-name <front-door-profile> --query originResponseTimeoutSeconds
az afd profile update --subscription <subscription-id> --resource-group <front-door-resource-group> --profile-name <front-door-profile> --origin-response-timeout-seconds 60A write that returned 504 might still have been applied. Before retrying an AppendDocument, which isn't idempotent, check the document with GetDocumentMetadata or CompareDocument.
To use a custom domain without Front Door, set only CUSTOM_DOMAIN_NAME and bind the domain to the Container App yourself.
After a fresh deployment, seed the shared usage guide so any AI tool connecting to the new instance knows how to use DevBrain.
-
Edit the Known Projects list in
docs/seed/ref-devbrain-usage.mdto name your organization's projects. -
From any connected MCP client (for example, Claude Code running in a clone of this repo), ask:
Read
docs/seed/ref-devbrain-usage.mdand store it in DevBrain withUpsertDocument, keyref:devbrain-usage, projectdefault, tagsmeta,instructions,usage.
Re-running is safe because every upsert is a full overwrite. Seeding goes through a signed-in client because every DevBrain tool call requires a per-user OAuth token with the DevBrain.User role.
DevBrain 2.0 removes the Azure Functions host. On an existing 1.x environment:
- On the existing Entra app, add the
DevBrain.Userapp role and assign it (step 1, item 3, and step 2). Then, in the existing azd environment, runazd provision, add the new redirect URI (step 4), and runazd deploy. The Container App reads and writes the same Cosmosdocumentscontainer, so no data migration is needed. - Point every MCP client at
https://<container-app-fqdn>/mcpin place ofhttps://<function-app>/runtime/webhooks/mcp. Each client signs in once against the new endpoint. - Once no client uses the old endpoint, delete the resources that
azd provisionno longer manages. Provisioning is incremental, so it doesn't remove them for you:- the
func-devbrain-<token>Function App and itsplan-devbrain-<token>Flex Consumption plan. Deleting the Function App also removes its system-assigned identity; Azure then shows that identity's old role assignments as "Identity not found", and you can remove them. - the
deploymentpackageanddataprotection-keysblob containers in the storage account
- the
- Remove the old Function App URL from the Entra app's redirect URIs.
1.x OAuth records left in the oauth_state container expire on their own through Cosmos TTL.
- Client secret expiry. When the Entra client secret expires, sign-ins and token refreshes fail. Before it expires, create a new secret and write it to the
entra-client-secretKey Vault secret, either withaz keyvault secret setor by settingENTRA_CLIENT_SECRETand re-runningazd provision. Container Apps picks up the new version within 30 minutes. To apply it immediately, restart the active revision withaz containerapp revision restart. - JWT signing secret rotation. Replacing
jwt-signing-secretinvalidates every DevBrain access token already issued, so clients re-authenticate on their next call. It's the "sign everyone out" lever. - Cold starts. The template defaults to zero minimum replicas, so the first request after an idle period waits for a container to start. Set
containerAppMinReplicasto1ininfra/main.bicepif interactive latency matters more than idle cost. Behind Front Door, keep the profile's origin response timeout longer than a cold start, or that first request can return504(see Optional: custom domain and Azure Front Door). - Cost profile. The main fixed costs are Cosmos DB, which uses standard provisioned throughput by default, and the Basic Azure Container Registry. Container Apps compute scales to zero by default. Check the Cosmos throughput settings against your budget before a long-running trial.
- Serverless Cosmos DB. If the subscription's one free-tier Cosmos account is already in use, run
azd env set COSMOS_SERVERLESS truebefore the firstazd provision. Cosmos DB then bills per request and per GB stored, which is usually far cheaper for a small team's knowledge store, at the cost of no throughput or latency guarantees. Cosmos DB fixes the capacity mode when it creates the account, so set this before the first provision. - Tearing down a trial. The Key Vault has purge protection on, so
azd downleaves it soft-deleted for 90 days and its name can't be reused in that time. For another trial, create a new azd environment name (azd env new) rather than re-provisioning the old one.
DevBrain uses OAuth 2.0 with Dynamic Client Registration (DCR). Clients that support the MCP OAuth spec connect with just a URL — no API keys, no manual configuration, no local proxies. The server handles registration, authorization, and token exchange automatically via the built-in DCR facade backed by your Entra tenant.
The examples below use the default Container Apps host name. If you set CUSTOM_DOMAIN_NAME, use that host instead; azd env get-value DEVBRAIN_MCP_URL prints the exact URL.
claude mcp add devbrain --transport http https://<CONTAINER_APP_FQDN>/mcpOn first use, Claude Code opens a browser for Entra login. Subsequent sessions re-use the stored token.
Add as a custom MCP connector pointing at:
https://<CONTAINER_APP_FQDN>/mcp
OAuth completes automatically — no proxy, no function key, no manual headers.
The modern unified ChatGPT/Codex app for Windows is currently working well with DevBrain OAuth. This is treated as operationally healthy but still under monitoring, rather than a permanent compatibility guarantee.
codex mcp add devbrain --transport http https://<CONTAINER_APP_FQDN>/mcpDevBrain rotates OAuth refresh tokens on every refresh. To tolerate brief client retry or restart races while a credential cache catches up, the just-rotated token remains a replay marker for a short period and returns the same replacement refresh token during that window.
Deployments can tune the access-token lifetime and refresh replay window when their client mix or operating environment needs a different refresh cadence. For example:
azd env set OAUTH_ACCESS_TOKEN_LIFETIME_MINUTES 45
azd env set OAUTH_REFRESH_REPLAY_LIFETIME_MINUTES 5
azd provisionazd provision applies the settings to the Container App and rolls a new revision. azd deploy only deploys a new application image, so existing settings persist across code-only updates. If the OAUTH_* values are not set, DevBrain uses its built-in defaults.
These azd values provision the equivalent application settings:
OAuth__AccessTokenLifetimeMinutes=45
OAuth__RefreshReplayLifetimeMinutes=5
For a one-off test on an already-provisioned app, set the same OAuth__* environment variables on the Container App directly, which creates a new revision. Both values must be whole minutes from 1 through 1,440. Defaults are 10 minutes for access tokens and 5 minutes for refresh replay markers.
Keep both windows as short as the client population allows. A longer access-token lifetime reduces refresh frequency but extends the useful lifetime of a stolen bearer token. A longer replay window makes an old refresh token reusable for longer and should only be used to accommodate a measured client retry interval.
DevBrain 2.0 owns the /mcp protocol surface directly and returns the specification-required 401 plus WWW-Authenticate: Bearer resource_metadata="..." challenge. That removes the Azure Functions host-layer limitation that stopped VS Code and GitHub Copilot from starting OAuth in 1.x. VS Code and GitHub Copilot now complete OAuth and use DevBrain's tools end to end.
https://<CONTAINER_APP_FQDN>/mcp
Add it as an HTTP MCP server. VS Code starts the Entra sign-in on first use.
Expected to work, because Cursor supports current MCP OAuth with DCR, but not yet validated. Connect it to https://<CONTAINER_APP_FQDN>/mcp.
DevBrain is only as useful as the context your AI tools actually load. The recommended pattern is a small AGENTS.md file at the repo root that tells any AI tool how to pull context from DevBrain at the start of a session.
Why AGENTS.md: GitHub Copilot, Cursor, and Codex all read AGENTS.md. Claude Code reads CLAUDE.md but can @import other files — so a one-line @AGENTS.md in CLAUDE.md keeps a single source of truth across every tool.
## DevBrain Session Startup
At the start of every session, load project context from DevBrain:
1. GetDocument(key="{your-project}:state:current", project="{your-project}")
2. If a sprint is active: GetDocument(key="{your-project}:sprint:{sprint-name}", project="{your-project}")
Before ending a session, write back any significant changes:
- UpsertDocument key="{your-project}:state:current" if project state changed
- UpsertDocument key="{your-project}:sprint:{name}" if sprint progress changed
Every key starts with the project name, and every call also passes the project.
DevBrain is the canonical source of truth. Do not ask the user to upload
files or paste context — read it directly from DevBrain.@AGENTS.mdNew to a project? See docs/project-init.md for the recommended first documents to seed.
All tools accept an optional project parameter (defaults to "default") to isolate documents by project.
| Tool | Inputs | Purpose |
|---|---|---|
UpsertDocument |
key (required), content (required), tags, project |
Create or replace a document by key |
AppendDocument |
key (required), content (required), separator, tags, project |
Append content to an existing document (or create it). Server-side concatenation; tag union. |
UpsertDocumentChunked |
key (required), content (required), chunkIndex (required), totalChunks (required), tags, project |
Upload a document in multiple chunks when it is too large to emit in a single LLM turn. |
GetDocument |
key (required), project |
Retrieve a document by key |
GetDocumentMetadata |
key (required), project |
Retrieve document metadata (tags, timestamps, contentHash, contentLength) without the content body |
CompareDocument |
key (required), content or contentHash (one required), project |
Check whether candidate content matches a stored document by SHA-256 hash |
PreviewEditDocument |
key (required), oldText (required), newText (required), expectedOccurrences, caseSensitive, project |
Preview a literal text replacement without writing; returns match count, before/after preview, and the current content hash |
ApplyEditDocument |
key (required), oldText (required), newText (required), expectedContentHash (required), expectedOccurrences, caseSensitive, project |
Apply a literal text replacement only if the document still matches the preview hash |
EditTags |
key (required), add, remove, project |
Add and/or remove tags on a document without re-emitting content. A tag in both add and remove is rejected. |
ListDocuments |
prefix, project |
List document keys, optionally filtered by prefix |
SearchDocuments |
query (required), project |
Substring search across keys and content |
DeleteDocument |
key (required), project |
Delete a document by key. Idempotent on missing keys. |
DevBrain still stores documents as whole values, but it now supports a safe two-step edit flow for exact text changes:
- Call
PreviewEditDocumentwith the literaloldTextandnewText - Inspect the returned
matchCount, preview snippets, andcurrentContentHash - Call
ApplyEditDocumentwith the same edit inputs andexpectedContentHash
Why two steps:
- Ambiguity guard. Preview refuses edits when the number of matches differs from
expectedOccurrences(defaults to1). - Concurrency guard. Apply fails if the stored content hash changed after preview, preventing stale overwrites.
- Agent-friendly ergonomics. Exact snippet replacement is more reliable than offsets or regex for most AI callers.
Example:
PreviewEditDocument(
key="devbrain:state:current",
project="devbrain",
oldText="Status: draft",
newText="Status: in progress"
)
ApplyEditDocument(
key="devbrain:state:current",
project="devbrain",
oldText="Status: draft",
newText="Status: in progress",
expectedContentHash="<hash from preview>"
)
EditTags applies a tag diff to a document, leaving content untouched. Pass add and/or remove as disjoint lists — a tag that appears in both is rejected. Already-present tags in add are no-ops; absent tags in remove are silently ignored (idempotent). Both lists empty returns a "nothing to do" message without a write.
EditTags(
key="ref:devbrain-usage",
project="default",
add=["workflow"],
remove=["draft"]
)
Use EditTags whenever you only need to adjust tag metadata — it avoids the overhead of sending the entire document body through UpsertDocument.
Both tools exist to work around the LLM-client per-turn output budget, but they solve different problems:
AppendDocument— for growing logs (session history, decision logs, audit trails). Each call adds a short entry to a document whose existing body the caller doesn't need to re-emit. Concurrent appenders are serialized via Cosmos ETag concurrency with bounded retry.UpsertDocumentChunked— for a single document that's too big to emit atomically. Callers split the content across calls with(chunkIndex, totalChunks); chunks may arrive out of order. The server concatenates on the final chunk and upserts the real key in one step. Abandoned uploads expire automatically.
Pick Append when the doc grows over time. Pick Chunked when you already have the whole thing and just can't fit it in one call.
Documents are organized by key prefix. These conventions are recommended but not enforced:
A key identifies one document across the whole store; project filters reads but isn't part of a document's identity. Start every project document's key with its project name (for example acme-platform:state:current), and still pass project on every call. Unprefixed keys belong to the default project, which holds general DevBrain material such as ref:devbrain-usage.
Keys use colon as the separator (e.g. acme-platform:sprint:license-sync). Writes (UpsertDocument, AppendDocument, UpsertDocumentChunked) reject keys containing / with a "did you mean" error suggesting the colon form. Reads (GetDocument, ListDocuments, SearchDocuments) and DeleteDocument continue to accept slash keys so legacy data and cleanup operations keep working.
| Prefix | Use |
|---|---|
{project}:sprint:{name} |
Sprint specs, e.g. acme-platform:sprint:license-sync |
{project}:state:current |
Current project state document |
{project}:arch:{name} |
Architecture docs |
{project}:decision:{name} |
Architecture decision records |
{project}:ref:{name} |
Project reference material, infra constants |
ref:{name} |
General DevBrain reference material in the default project |
-
Install the .NET 10 SDK and Azure CLI.
-
Sign in to Azure so
DefaultAzureCredentialcan reach the data services:az login
Point local runs at a dev DevBrain environment, not production. Your identity needs Cosmos DB Built-in Data Contributor (a Cosmos SQL role assignment), Storage Blob Data Contributor on the storage account, and Key Vault Crypto User on the vault.
-
Configure the required settings with .NET user secrets. The server fails fast at startup if any of these is missing:
$p = 'src/DevBrain.Server' dotnet user-secrets --project $p set CosmosDb:AccountEndpoint 'https://<cosmos-account>.documents.azure.com:443/' dotnet user-secrets --project $p set OAuth:BaseUrl 'http://localhost:5000' dotnet user-secrets --project $p set OAuth:EntraTenantId '<tenant-guid>' dotnet user-secrets --project $p set OAuth:EntraClientId '<app-client-id>' dotnet user-secrets --project $p set OAuth:EntraClientSecret '<client-secret>' dotnet user-secrets --project $p set OAuth:JwtSigningSecret ([Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))) dotnet user-secrets --project $p set DataProtection:BlobUri 'https://<storage-account>.blob.core.windows.net/dataprotection-keys-v2/keys.xml' dotnet user-secrets --project $p set DataProtection:KeyVaultKeyUri 'https://<key-vault>.vault.azure.net/keys/data-protection-key'
Add
http://localhost:5000/callbackas a second Web redirect URI on the Entra app to sign in against the local server. -
Run the server:
dotnet run --project src/DevBrain.Server
The local MCP endpoint is
http://localhost:5000/mcp, and/healthzis anonymous. -
Run the tests. They use xUnit v3 on Microsoft.Testing.Platform, which
global.jsonopts into:dotnet test --solution devbrain.slnx -
Optional dependency health checks from the repository root:
dotnet list devbrain.slnx package --vulnerable --include-transitive dotnet list devbrain.slnx package --outdated dotnet list devbrain.slnx package --deprecated --include-transitive
DevBrain implements RFC 7591 Dynamic Client Registration (DCR) with an in-process OAuth proxy that brokers a single pre-registered Entra app. From the client's perspective, DevBrain is the authorization server. Internally it delegates to your tenant's Entra ID for user authentication.
The 2026-07-28 specification deprecates DCR in favor of Client ID Metadata Documents but retains it for backward compatibility. DevBrain keeps DCR for the clients in its compatibility matrix while honoring the revision's authorization hardening: application_type metadata, RFC 8707 resource binding, and RFC 9207 issuer identification on authorization responses.
This solves two problems that previously blocked MCP OAuth:
- Entra doesn't support DCR — DevBrain's facade implements it, issuing opaque
client_idhandles that all map to the same upstream Entra app. - Claude.ai ignores external IdP endpoints in discovery metadata — DevBrain hosts its own
/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resourceon its own domain.
Every write operation records the authenticated user's Entra UPN in the updatedBy field.
The deployment is intentionally single-tenant. Validated Entra roles claims are carried into the local DevBrain session, and /mcp requires the DevBrain.User app role. There is no application-level administrator role or maintenance endpoint; administration is performed through Azure and Entra control planes.
Access tokens are short-lived and DevBrain refresh tokens rotate on every refresh. By default, the old refresh token becomes a five-minute replay marker that points at the replacement token, which makes immediate MCP client retries idempotent without reopening the OAuth flow. Replays outside the configured window still fail with invalid_grant, and every successful refresh or replay extends the upstream token vault record for the same local refresh window. See OAuth token windows for configuration and security tradeoffs.
The first use of each rotated refresh token also refreshes the upstream Entra session and revalidates tenant, user identity, and app-role claims. Assignment changes therefore take effect when the current short-lived access token expires rather than remaining cached for the full local refresh-token lifetime.
The 2.0 host keeps the same OAuth DCR flow as 1.x and fixes the VS Code/Copilot challenge blocker. "Working" means the client works with DevBrain's OAuth flow in production use.
| Client | Platform | Auth | Status |
|---|---|---|---|
| Claude Code CLI | Windows Terminal | OAuth (DCR) | ✅ Working |
| Claude Code CLI | WSL | OAuth (DCR) | ✅ Working |
| Claude Code | claude.ai web | OAuth (DCR) | ✅ Working |
| Claude Desktop | Windows | OAuth (DCR) | ✅ Working |
| Claude Mobile | Android | OAuth (DCR) | ✅ Working |
| ChatGPT / Codex unified app | Windows | OAuth (DCR) | ✅ Working; monitoring continues |
| Codex CLI | Windows Terminal | OAuth (DCR) | ✅ Working |
| Codex CLI | WSL | OAuth (DCR) | ✅ Working |
| VS Code / GitHub Copilot | Windows | OAuth (DCR) | ✅ Working (new in 2.0) |
| Cursor | — | OAuth (DCR) | Expected to work; not yet validated |
See CONTRIBUTING.md for guidelines, PR process, and local dev setup.
MIT — Ignite Solutions Group