Skip to content

Add RFC 7591 Dynamic Client Registration for MCP OAuth#343

Closed
manana2520 wants to merge 69 commits intoAIDotNet:mainfrom
keboola:feature/dynamic-client-registration
Closed

Add RFC 7591 Dynamic Client Registration for MCP OAuth#343
manana2520 wants to merge 69 commits intoAIDotNet:mainfrom
keboola:feature/dynamic-client-registration

Conversation

@manana2520
Copy link
Contributor

Summary

Implements OAuth 2.0 Dynamic Client Registration (RFC 7591) for the MCP OAuth authorization server. This allows MCP clients like Claude Code CLI to automatically register themselves before starting the OAuth flow.

Changes

  • New endpoint: POST /oauth/register accepts client metadata (redirect_uris, client_name), validates URIs, generates a unique client_id, and returns an RFC 7591 compliant response (HTTP 201)
  • Updated metadata: /.well-known/oauth-authorization-server now includes registration_endpoint
  • Redirect URI validation: The /oauth/authorize endpoint now accepts redirect URIs from dynamically registered clients in addition to well-known URIs. Allows http://localhost for CLI tools.
  • In-memory storage: Client registrations are stored in memory (consistent with existing auth code/pending authorization storage)

Why

Claude Code CLI and other MCP clients require dynamic client registration to connect to MCP servers. Without a registration_endpoint in the authorization server metadata, clients fail with "Incompatible auth server: does not support dynamic client registration".

Security

  • Only https:// and http://localhost redirect URIs are accepted
  • Redirect URIs are validated per-client during authorization
  • Well-known URIs (claude.ai) continue to work without registration

Test plan

  • /.well-known/oauth-authorization-server includes registration_endpoint
  • POST /oauth/register returns 201 with valid client registration
  • Invalid redirect URIs are rejected (non-https, non-localhost)
  • Registered client can authorize with their declared redirect URI
  • Claude Code CLI successfully connects to MCP server

manana2520 and others added 30 commits February 19, 2026 13:34
Change all hardcoded 'zh' (Chinese) locale defaults to 'en' (English)
across the frontend: i18n config, middleware, profile API defaults,
repository submit form, and settings page.
npm ci requires exact lock file match which fails when building
from a clean clone with potentially outdated lock file.
- Add /auth/callback page to handle OAuth redirect from Google
- Implement handleOAuthLogin() in auth page (was TODO stub)
- Replace GitHub+Google buttons with Google-only button
Replace hardcoded English strings with t() calls using the existing
next-intl i18n system. Added new translation keys for the overview
cards (completion rate, public ratio, selection ratio, status
distribution) in all 4 locales (en, zh, ko, ja).
Create organizations.json translation files for all 4 locales
(en, zh, ko, ja) and register the new namespace in the i18n
config and useTranslations hook. Replace all hardcoded strings
in the organizations page with t() calls.
Replace all hardcoded Chinese strings in the API proxy route
including error messages, log messages, and code comments.
Add ~100 translation keys under admin.repositories.management
namespace for all 4 locales (en, zh, ko, ja). This fixes the
repository detail/management page showing raw translation keys
instead of translated text.
The prompts/ directory containing AI prompt templates (catalog-generator.md,
content-generator.md, etc.) was not being copied to the publish output,
causing FileNotFoundException when processing repositories.

Added CopyToOutputDirectory directive matching the existing skills/ pattern.
All API response messages (error messages, status text, operation results)
were in Chinese. Translated to English since the admin panel is used by
English-speaking team members only.
- Root layout meta description (fixes Slack link preview showing Chinese)
- Share page title and UI strings
- Settings page fallback strings and comments
- Embed.js comments, error messages, and UI strings
- New GitHubAppInstallation entity for storing app installations per org
- GitHubAppService: JWT generation, installation token management, repo listing
- Admin endpoints: GET /github/status, POST /installations, GET /repos, POST /batch-import
- RepositoryAnalyzer: credential fallback chain (per-repo > app token > global token)
- Frontend: GitHub Import admin page with org connection, repo browsing, batch import
- Navigation: Added GitHub Import link in admin sidebar
- i18n: Added githubImport translation keys for all 4 locales
RepositoryService now allows private repos without per-repo credentials
when the organization has a GitHub App installation configured. This
enables the batch import flow where repos are cloned using installation
tokens instead of individual PATs.
Add explicit fallback to read GitHub__App__* and GITHUB_APP_* env vars
directly when IConfiguration hierarchical lookup fails.
Fall back to /data/logs when /app/logs is not writable (container runs
as non-root), and skip file sink entirely if no writable directory is
available.
Adds a search box (filters by repo name and description) and a
programming language dropdown filter to help navigate large
organizations with hundreds of repositories.
Fetches all installation repos in batches of 100, then does filtering
and pagination entirely client-side. Search and language filter now
work across all 950+ repos, not just the visible page.
GitHub App integration for org-wide repository import
On mobile (<768px), the sidebar is now hidden behind a hamburger menu
that opens as a slide-in Sheet overlay. Navigation links auto-close
the sheet. The confusing back arrow is replaced with a clear
"Back to Home" link at the bottom of the nav.
Implement MCP (Model Context Protocol) server endpoint at /api/mcp that
exposes repository documentation as tools for AI clients (Claude, Cursor).

- McpRepositoryTools: 4 tools (ListRepositories, GetDocumentCatalog,
  ReadDocument, SearchDocuments) with department-based access control
- McpAuthConfiguration: Google OAuth token validation + Protected Resource
  Metadata (RFC 9728) for MCP client discovery
- McpUserResolver: Maps Google/JWT tokens to DB users via email claim
- SseKeepAliveMiddleware: Prevents client disconnections on long requests
- Update MCP SDK from 0.6.0-preview.1 to 0.8.0-preview.1
Replace custom admin sidebar with the same shadcn/ui Sidebar component
system used by the main page. Adds a toggle button (PanelLeft icon) in
the admin header to collapse the sidebar to icon-only view. Mobile
sidebar is now handled automatically by the Sidebar component.
Add responsive mobile sidebar for admin panel
Claude.ai and other MCP clients expect the MCP server to act as its own
OAuth authorization server. This implements the full OAuth 2.1 flow that
proxies through Google OAuth:

- /.well-known/oauth-authorization-server: RFC 8414 metadata discovery
- /oauth/authorize: Redirects to Google OAuth with PKCE
- /oauth/callback: Handles Google redirect, exchanges code, issues auth code
- /oauth/token: Exchanges auth code for internal JWT (validates PKCE)
- Next.js proxy routes for /.well-known/* and /oauth/* paths
- In-memory auth code store with background cleanup
- Updated protected resource metadata to point to self as auth server

Requires GOOGLE_CLIENT_SECRET env var alongside existing GOOGLE_CLIENT_ID.
- Change OAuth metadata to use /oauth/authorize and /oauth/token paths
  (matching frontend's /oauth/* catch-all proxy route)
- Remove root-level /authorize, /callback, /token endpoints that
  weren't reachable through the Next.js frontend proxy
- Make MCP server registration conditional on GOOGLE_CLIENT_ID and
  GOOGLE_CLIENT_SECRET being configured, so the backend starts
  normally without MCP when these aren't set
manana2520 and others added 28 commits February 21, 2026 17:54
The backend already supports Slack but the frontend dropdown was missing it,
forcing admins to use the Custom option with raw JSON. This adds Slack with
proper form fields (Bot Token, Signing Secret, API Base URL, Reply in Thread)
matching the backend SlackProviderOptions.
* Add provider-based model loading to Tools/Models page

The create/edit dialog now mirrors the Settings Quick Setup flow:
select provider, enter API key, click Load Models to fetch available
models from the provider API, then select from a dropdown instead of
typing model IDs manually. Display name auto-fills from selection.

* Translate Chinese error messages to English in chat services

Replace all user-facing Chinese strings with English in
ChatAssistantService, EmbedService, ChatShareService, and
McpToolConverter. Covers error messages for disabled assistant,
unavailable models, invalid apps, domain validation, MCP errors,
share limits, and quoted text labels.
* Add GitHub App credentials configuration via admin UI

Instead of requiring manual SSH access to set environment variables,
admins can now configure GitHub App credentials (App ID, name, and
private key) directly from the /admin/github-import page. Credentials
are validated against the GitHub API before saving to the database
and take effect immediately via an in-memory singleton cache.

Key changes:
- GitHubAppCredentialCache singleton for DB-backed credential resolution
- GET/POST /api/admin/github/config endpoints for reading/saving config
- Configuration form with App ID, App Name, and Private Key fields
- Credential validation against GitHub API before persisting
- Support for updating credentials (private key optional when updating)
- Env vars continue to work as fallback for existing deployments
- i18n translations for all 4 locales

* Hide config form when configured, add org disconnect button

- Config form only shown when GitHub App is not configured
- Add disconnect button per connected organization (soft-delete)
- Add DELETE /api/admin/github/installations/{id} endpoint
- Add i18n keys for disconnect UI

* Make disconnect button more visible with outline style and label

* Add reset GitHub App configuration feature

- DELETE /api/admin/github/config endpoint to clear credentials
- Clear() method on GitHubAppCredentialCache singleton
- Reset button in admin UI next to Connect New Organization
- Soft-deletes SystemSettings with category "github"

* Add PEM file upload button for private key field

* Fix UNIQUE constraint error when re-saving config after reset

UpsertSettingAsync now finds soft-deleted rows and un-deletes them
instead of inserting duplicates that violate the Key unique constraint.

* Fix UNIQUE constraint when reconnecting a previously disconnected org

StoreInstallationAsync now finds soft-deleted installations and
re-activates them instead of inserting duplicates.
- Frontend: ~500 user-facing strings, error messages, and comments in 45+ files
- Backend: ~200 API response messages, error strings, and log messages in 33+ files
- Swagger/OpenAPI: 82 WithSummary, 8 WithDescription, 18 WithTags, 9 Tags attributes
- XML doc comments: ~3000 lines across 220+ C# files (Entities, Chat, Models, Endpoints, Services, Agents)
- Data annotation validation messages in Models
- Only intentional Chinese remaining: backward-compatible regex patterns in ProcessingLogService.cs matching legacy database log entries
…-to-english

Translate all Chinese text to English
Introduces IConfigurableProvider interface and ProviderConfigApplicator
hosted service that bridges database config to live provider instances.

SlackProvider now supports runtime config updates from the admin UI:
- Volatile options pattern for thread-safe config swaps
- Falls back to env vars when no DB config exists
- Re-initializes (validates token) on config change

ChatConfigService now fires IConfigChangeNotifier on save/delete for
immediate propagation (instead of waiting for ConfigReloadService poll).
Also adds Slack to the required fields validation.
Shows where to find each value in Slack App settings:
- Bot Token: OAuth & Permissions > Bot User OAuth Token
- Signing Secret: Basic Information > App Credentials
- API Base URL: default hint
…nfig

ChatUserResolver was reading Slack BotToken from IOptions<SlackProviderOptions>
(env vars / appsettings.json) which is empty when config is managed via the
admin UI (stored in database). This caused user identity resolution to fail,
making the Slack bot operate in public-only mode and report zero repositories.

Now reads BotToken and ApiBaseUrl from the live SlackProvider instance registered
in IMessageRouter, which reflects database-backed config updates via
the IConfigurableProvider / ProviderConfigApplicator mechanism.
# Conflicts:
#	src/OpenDeepWiki/Chat/Config/ChatConfigService.cs
- Fix action icons overlapping title in provider cards (min-w-0, truncate, shrink-0)
- Fix admin-api to handle both array and {data: ...} response formats
Show helper text below each Slack configuration field explaining
where to find the value in Slack App settings.
…IDotNet#13)

- Select All checkbox now selects only repos on the current page
- When all page repos selected, shows Gmail-style banner to select all
  matching repos across all pages
- Clicking "Select all N matching repositories" selects everything
- Add import status filter dropdown (All / Not Imported / Already Imported)
- Checkbox shows indeterminate state when partially selected
- i18n translations added for all 4 locales (en, zh, ko, ja)
* Add user-level GitHub repository import

Allow authenticated non-admin users to browse and import repositories
from GitHub organizations that admins have already connected via the
GitHub App. Users can choose to import repos as "personal only" (visible
only to them) or assign to one of their departments.

Backend:
- New UserGitHubImportService with status, repo listing, and import
- New /api/github endpoints (status, installations/repos, import)
- Fix McpUserResolver to let owners access their own private repos

Frontend:
- Extract GitHubInstallationList and GitHubRepoBrowser shared components
- Refactor admin GitHub import page to use shared components
- New user import page at /private/github-import
- Add "Import from GitHub" button to /private page
- i18n strings for all 4 locales (en, zh, ko, ja)

* Fix SelectItem empty value bug in department selector

Radix UI Select does not support empty string values for SelectItem.
Use a sentinel value PERSONAL_ONLY_VALUE instead of "" for the
"Personal only" option, converting back to empty string when
calling the import API.
Make the "Integrate MCP" button on the dashboard open a modal
showing Slack integration status (with dynamic detection via API)
and MCP server URL with copy buttons and Claude Desktop config snippet.
…ss control (AIDotNet#15)

* Rename visibility toggle from Public/Private to Shared/Restricted

Remove password requirement for toggling visibility - any repo can now
freely switch between Shared and Restricted. GitHub App imports default
to Shared (visible to all org members). Update i18n labels in all 4
locales and simplify the VisibilityToggle component.

* Revert import default: respect GitHub repo privacy for IsPublic

IsPublic=true for private GitHub repos exposes them to non-logged-in
users. Revert to IsPublic = !repo.Private so private repos default
to Restricted and public repos default to Shared.

* SECURITY: Add access control to wiki document endpoints

Restricted (IsPublic=false) repositories were accessible to anyone
with a direct URL. Add access control checks to GetBranchesAsync,
GetTreeAsync, GetDocAsync, and ExportAsync that verify:
- Public repos: accessible to everyone
- Restricted repos: require authentication + (owner OR department member)

Non-authorized access returns empty/not-found responses (no information
leakage about repository existence).

* Include department repos in user repository list

The /private page only showed repos owned by the current user. Repos
imported by an admin and assigned to a department were invisible to
other department members because the query filtered strictly by
OwnerUserId.

Now when listing repos for a user, also include repos assigned to
departments the user belongs to (via RepositoryAssignment + UserDepartment).

* Sync repository visibility with GitHub API

- Add IsPrivate to GitRepoInfo record, parse from GitHub API response
- Server-side visibility override in SubmitAsync (verify against GitHub)
- Periodic visibility sync in IncrementalUpdateWorker
- Visibility sync during admin stats sync (single + batch)
- Fix frontend submit form: respect toggle state instead of deriving from password
- Auto-detect visibility from GitHub API when entering repo URL
- Add isPrivate field to GitRepoCheckResponse (backend + frontend)
…ub-filters (AIDotNet#17)

## Summary

Replaces the separate public/private repository pages with a unified dashboard featuring:

- **4 view tabs**: All, Public, Organization, My Repos
- **Dual icons**: Globe + Building2 for repos that are both public and in a department
- **Sub-filter chips**: contextual filters within each view (Public only, Private only, Shared/Not shared)
- **Admin restrict/unrestrict**: toggle visibility of repos for non-admin users
- **Share toggle**: users can share personal repos with their organization
- **Race condition protection**: loadIdRef counter prevents stale async responses
- **Stale state clearing**: view switches immediately clear previous data

### Backend changes
- Organization endpoints: share, unshare, restrict, unrestrict
- OrganizationService with department-based repository access
- DepartmentRepositoryInfo DTO with IsPublic, IsRestricted fields

### Frontend changes  
- Unified PublicRepositoryList with 4 views + sub-filters
- PublicRepositoryCard with dual icons and context-aware toggles
- Organization API client with fetchWithAuth for admin endpoints
- i18n keys for all new UI text (en, zh, ko, ja)
Private repos returned "Repository Not Found" on direct navigation because
SSR fetches had no Authorization header. JWT was stored only in localStorage,
which is inaccessible during server-side rendering.

- Store JWT in cookie alongside localStorage on login/logout
- Add getServerToken() to read cookie via next/headers during SSR
- Add getSSRAuthHeaders() helper in repository-api.ts
- Inject auth headers in all SSR fetch functions (fetchRepoTree,
  fetchRepoBranches, fetchRepoDoc, checkGitHubRepo, fetchRepoStatus,
  fetchProcessingLogs, fetchRepositoryList, fetchGitBranches, fetchMindMap)

On client side, getServerToken() returns null (window exists) so client-side
auth continues to work via apiClient and localStorage as before. No backend
changes needed - backend already handles Bearer tokens correctly.
getServerToken() was calling cookies() synchronously, but in Next.js 15+
cookies() returns a Promise. The function got a Promise object instead of
the cookie store, so the token was never read during SSR.

- Make getServerToken() async with await cookies()
- Make getSSRAuthHeaders() async
- Await getSSRAuthHeaders() in all fetch call sites
GitHub App batch imports incorrectly set OwnerUserId to the importing
admin, making all org repos appear in "My Repos". Restricting a repo
in the org view caused it to leak into My Repos because the frontend
subtraction hack broke when RepositoryAssignment was soft-deleted.

Changes:
- Add IsDepartmentOwned boolean to Repository entity
- Set IsDepartmentOwned=true in AdminGitHubImportService.BatchImportAsync
- Exclude department-owned repos from ownerId queries in GetListAsync
- Add admin permission fallback for dept-owned repos (visibility, regenerate)
- Fix unique index: (OwnerUserId, OrgName, RepoName) -> (OrgName, RepoName)
- Frontend: remove subtraction hack and sub-filters from Mine view
- Frontend: remove share toggle from Mine view cards
UserGitHubImportService.ImportAsync also creates department-linked
repos when a DepartmentId is provided. Without setting IsDepartmentOwned,
these repos would appear in both My Repos and Organization view.
When the GitHub installation is linked to a department, hide the
"Personal only" option and auto-select that department. Org repos
should always be imported as department-owned.
Adds a PUT endpoint /api/admin/github/installations/{id}/department
to link/unlink a GitHub App installation to a department. The admin
GitHub import page now shows a department dropdown on each installation
row, allowing admins to manage the department link directly.

This replaces the need for manual DB updates when connecting GitHub
organizations to DeepWiki departments.
Claude Code CLI requires dynamic client registration to connect to
MCP servers. The OAuth authorization server now supports POST /oauth/register
where clients can register their redirect_uris and obtain a client_id.

Changes:
- Add POST /oauth/register endpoint accepting client metadata
- Advertise registration_endpoint in authorization server metadata
- Accept http://localhost redirect URIs for CLI tools
- Validate redirect_uris against registered client during authorization
- Store client registrations in memory (lost on restart = clients re-register)
@239573049
Copy link
Member

Need help to handle the conflict.

@239573049 239573049 closed this Mar 24, 2026
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.

2 participants