A B/S multimodal AI chat platform based on the DeepSeek Harness principles: LLM + MCP (Model Context Protocol) + Skill plugin orchestration, with a Cordis-driven, Three.js-powered 3D frontend.
Reference paper: A Programming Paradigm for Spatiotemporal Composability — everything is a plugin, driven by Cordis.
Multilingual UI: English (default) and 简体中文 — switch from the top bar (🌐 EN / 中文); your choice is remembered in localStorage.
erDiagram
APP_USER {
Guid id PK
string auth_type "default=password account; acs/ucs…=internal auth"
string username "unique together with auth_type"
string display_name
string email
string password_hash
string password_salt
string status
datetime created_at
datetime last_login_at
}
APP_ROLE {
Guid id PK
string name
string code UK
string description
bool is_system
datetime created_at
}
CHAT_SESSION {
Guid id PK
Guid user_id FK
string title
string status
Guid llm_provider_id
string context_json
datetime created_at
datetime updated_at
datetime last_message_at
}
CHAT_MESSAGE {
Guid id PK
Guid session_id FK
Guid user_id FK
string role
text content
text reasoning
string tool_calls_json
string status
string model
int prompt_tokens
int completion_tokens
string trace_id
string client_message_id UK
datetime created_at
}
LLM_PROVIDER {
Guid id PK
string name
string kind
string base_url
string api_key_encrypted
int priority
bool enabled
bool is_healthy
datetime created_at
datetime updated_at
}
LLM_MODEL {
Guid id PK
Guid provider_id FK
string name
bool enabled
bool is_vision
int context_window
decimal price_in_per_1k
decimal price_out_per_1k
int priority
datetime created_at
}
INTERNAL_AUTH_PROVIDER {
Guid id PK
string name "acs / ucs…, unique"
string api "auth center URL"
string http_method
string request_format "body(application/json)"
string username_field
string password_field
bool enabled
int timeout_seconds
datetime created_at
}
INTERNAL_AUTH_SUCCESS_RULE {
Guid id PK
Guid provider_id FK
string field "JSON path, e.g. sessionID"
string operator "NotEmpty / Equals"
string expected_value
}
MCP_SERVER {
Guid id PK
string name
string transport
string endpoint
string headers_json
string stdio_command
bool enabled
bool is_vision
int timeout_seconds
string description
datetime created_at
}
MCP_CATALOG_ITEM {
Guid id PK
Guid mcp_server_id FK
string kind
string name
string description
string schema_json
bool enabled
}
PROMPT {
Guid id PK
string name
string description
text content
bool enabled
int version
datetime created_at
}
SKILL {
Guid id PK
string name
string meta_tool_name
text instruction
bool enabled
string model_override
int max_nested_steps
}
TOOL_APPROVAL {
Guid id PK
string trace_id
Guid user_id FK
Guid session_id FK
string mcp_server_name
string tool_name
string arguments_json
string status
datetime created_at
datetime decided_at
datetime expires_at
}
AUDIT_LOG {
Guid id PK
string trace_id
Guid user_id FK
string category
string action
string target
string detail_json
string ip
bool is_suspicious
datetime created_at
}
USER_SETTING {
Guid id PK
Guid user_id FK
string key
string value_json
}
TOKEN_USAGE_RECORD {
Guid id PK
string trace_id UK
Guid user_id FK
Guid session_id FK
string provider_name
string model
int total_tokens
decimal cost
int ttft_ms
datetime created_at
}
APP_USER ||--o{ CHAT_SESSION : "owns"
APP_USER ||--o{ CHAT_MESSAGE : "sends"
APP_USER }o--o{ APP_ROLE : "member_of (user_roles)"
APP_USER ||--o{ USER_SETTING : "has"
APP_ROLE }o--o{ MCP_SERVER : "can_use (role_mcp_servers)"
APP_ROLE }o--o{ PROMPT : "can_use (role_prompts)"
APP_ROLE }o--o{ SKILL : "can_use (role_skills)"
APP_ROLE }o--o{ LLM_MODEL : "can_use (role_model_bindings)"
APP_ROLE }o--o{ INTERNAL_AUTH_PROVIDER : "default roles (internal_auth_provider_roles)"
LLM_PROVIDER ||--o{ LLM_MODEL : "models"
INTERNAL_AUTH_PROVIDER ||--o{ INTERNAL_AUTH_SUCCESS_RULE : "success rules"
MCP_SERVER ||--o{ MCP_CATALOG_ITEM : "catalog"
CHAT_SESSION ||--o{ CHAT_MESSAGE : "contains"
CHAT_SESSION }o--o| LLM_PROVIDER : "uses"
TOOL_APPROVAL }o--|| APP_USER : "requested_by"
TOOL_APPROVAL }o--|| CHAT_SESSION : "in"
AUDIT_LOG }o--o| APP_USER : "actor"
TOKEN_USAGE_RECORD }o--o| APP_USER : "accrued_by"
flowchart LR
subgraph Web["Web (Vue 3 + Cordis)"]
UI[Chat UI / 3D Theme] --> K[Kernel Plugins]
K -->|SSE / REST| HTTP[Vite proxy → /api]
end
subgraph Api["NextChats.Api (ASP.NET Core)"]
Ctl[Controllers / RBAC / Audit] --> Mod[Error Localization Middleware]
Ctl --> Orch{{ChatOrchestrator}}
Ctl --> Adm[Admin: Providers / MCP / Prompts / Skills / RBAC / Internal Auth / Approvals]
Ctl --> Auth[Auth: default password / internal auth]
Auth -->|HTTP| IA[(Internal Auth Center acs / ucs…)]
end
Orch --> Loop{{AgentLoopEngine}}
Loop -->|ReAct think→act→observe| Router[LLM Router]
Router --> OpenAI[OpenAI-Compatible Client]
Router --> Mock[Mock Client: localized output]
Orch --> Driver[MCP Driver]
Driver --> MCP1[(MCP Server: Demo 5300)]
Driver --> MCP2[(MCP Server: more…)]
Orch --> Skills[Skill Execution Engine]
Orch --> Policy[Policy Engine / Approval Coordinator]
Api --> DB[(SQLite: config / chat / audit)]
UI --> V3[Three.js scenes]
style Orch fill:#e3f2fd
style Loop fill:#fff3e0
sequenceDiagram
participant U as User (Browser)
participant K as Cordis Kernel
participant C as ChatController
participant O as ChatOrchestrator
participant L as AgentLoopEngine
participant LLM as LLM Client
participant M as MCP Driver / Skills
U->>K: type message (+ optional images)
K->>C: POST /api/chat/stream (SSE)
C->>O: ChatStreamRequest (lang, images)
O-->>C: SESSION_NOT_FOUND? / INPUT_FLAGGED? (error event)
O->>L: AgentRunRequest (tools, context window)
loop ReAct round
L->>LLM: stream prompt + tools
LLM-->>L: reasoning / text / tool calls chunks
L-->>U: thinking_start · thinking_delta · text_delta (SSE)
alt tool call
L->>Policy: evaluate (allow/deny/approval)
Policy-->>U: approval_updated (pending) → user approves
L->>M: execute tool (approval / args)
M-->>L: tool result
L-->>U: tool_start · tool_result (SSE)
end
end
L-->>U: done (usage: tokens / ttft / cost)
O->>DB: persist assistant message + usage + audit (trace_id)
O-->>C: SSE end event
C-->>K: event stream → UI renders
sequenceDiagram
participant U as User (Browser)
participant B as ChatInputBar
participant C as ChatController
participant O as ChatOrchestrator
participant V as Vision MCP Tool (image_source)
participant LLM as LLM (Mock / real)
participant P as Provider · MCP Vision Flags
U->>C: GET /api/chat/vision-config
C->>P: provider.IsVision ∨ bound MCP.IsVision
P-->>U: supported: true → show upload button + Ctrl+V
U->>B: paste (Ctrl+V) or upload N images
B->>B: validate type/size → dataURL → base64
B->>C: POST /api/chat/stream { message, images[] }
C->>O: ChatStreamRequest.Images (standard base64)
loop each image (multi-image recognized one by one)
O->>V: call vision tool { image_source: base64 }
V-->>O: recognition text (per image)
end
O->>LLM: system + history + [vision texts] + user(message + <image_source> blocks)
LLM-->>U: streamed answer (SSE) using the recognized text
| Layer | Technology |
|---|---|
| Backend | .NET 10 · ASP.NET Core · EF Core (SQLite now → MySQL 8 ready) |
| MCP | ModelContextProtocol SDK 2.2.0 (latest stable) · Streamable HTTP transport · STDIO extensible |
| Frontend | Vue 3 · TypeScript · Vite · Element Plus · Three.js · Cordis ^3.18.1 (plugin kernel) |
| Security | JWT dual-token (30-min access + rotating refresh, revoked on disable/logout) · PBKDF2-SHA256(210k) password hashing · AES-256-GCM secret encryption · sanitized audit logs |
| i18n | vue-i18n 10 (en / zh-CN, English default) · X-Lang error localization on the API |
next-chats/
├── src/
│ ├── NextChats.Core/ # Domain model, orchestration, engines, drivers (no ASP.NET dependency)
│ ├── NextChats.Infrastructure/ # EF Core data, caching, security services (swappable)
│ └── NextChats.Api/ # Web API (SSE streaming, admin, RBAC, audit, metrics, i18n)
├── samples/McpDemoServer/ # Demo MCP Server (Streamable HTTP)
├── e2e/ # Internal-auth E2E (mock auth center + PowerShell suite)
└── web/ # Vue 3 + Cordis frontend
├── src/i18n/ # en / zh dictionaries (per-domain locale files)
└── scripts/smoke*.mjs # End-to-end smoke scripts (Node fetch streaming)
# 1. Start the demo MCP Server (port 5300)
dotnet run --project samples/McpDemoServer -c Debug
# 2. Start the API (port 5210; DB is auto-created and seeded on first boot)
dotnet run --project src/NextChats.Api -c Debug
# 3. Start the frontend (Vite proxies /api → 5210)
cd web && npm install && npm run dev
# Open http://localhost:5173 (seeded admin: admin / admin123)Running the published assembly directly (no IIS, no dotnet run) means the listening address comes from --urls:
dotnet NextChats.Api.dll --urls "http://*:6500"| Port source | Example | Priority |
|---|---|---|
Command line --urls |
dotnet NextChats.Api.dll --urls "http://*:6500" |
highest |
ASPNETCORE_URLS env var |
$env:ASPNETCORE_URLS="http://*:6500" |
middle |
launchSettings.json → applicationUrl |
only applies to dotnet run (this repo: 53803 / 53805) |
— |
appsettings.json → root "Urls" or Kestrel:Endpoints |
persistent default | low |
| Kestrel default | http://localhost:5000 |
lowest |
localhost binds the loopback adapter only. --urls "http://localhost:6500" listens on 127.0.0.1 / ::1, so http://<hostname>:6500/ (which resolves to a NIC address, e.g. 10.0.5.179) is answered with net::ERR_CONNECTION_REFUSED. The SPA shell may still render from browser cache and then fail on uncached assets (Unable to preload CSS for /assets/...). Bind all interfaces instead:
dotnet NextChats.Api.dll --urls "http://*:6500" # dual-stack: IPv6 + IPv4
dotnet NextChats.Api.dll --urls "http://0.0.0.0:6500" # IPv4 only
dotnet NextChats.Api.dll --urls "http://[::]:6500" # IPv6 dual-stackVerify the listener is 0.0.0.0 / :: (not 127.0.0.1 / ::1) and that the hostname answers:
Get-NetTCPConnection -LocalPort 6500 | Select-Object LocalAddress, State
curl.exe -s -o NUL -w "%{http_code}" "http://<hostname>:6500/"Run the DLL from its own directory (appsettings.json and the SQLite path data/nextchats.db are relative), and only one process can own a port — stop the IIS site first if it is still bound to the same port. IIS binds all addresses by default, which is exactly why the same hostname URL works under IIS but fails with --urls "http://localhost:6500".
The image bundles the Vue frontend and the .NET API: the API serves the SPA itself, so one container = the whole product (UI + API on port 8080). State lives in named volumes (SQLite DB, chat attachments, logs).
# 1. Create .env with two REQUIRED secrets (compose refuses to start without them)
cp .env.example .env
# edit .env, e.g.: NEXTCHATS_JWT_KEY=$(openssl rand -base64 48)
# NEXTCHATS_ENCRYPTION_KEY=$(openssl rand -base64 32)
# 2. Build & run
docker compose up --build -d
# 3. Verify, then open http://localhost:8080 (seeded admin: admin / admin123 — change it right away)
curl http://localhost:8080/health| Item | Details |
|---|---|
| Port | NEXTCHATS_PORT (host, default 8080) → container 8080 |
| Secrets | NEXTCHATS_JWT_KEY (signs login tokens) and NEXTCHATS_ENCRYPTION_KEY (encrypts stored LLM/MCP keys). Keep the encryption key stable — after changing it, stored keys can no longer be decrypted |
| Data (volumes) | next-chats-data → /app/data (SQLite, back this up), next-chats-uploads → /app/uploads, next-chats-logs → /app/logs |
| Config | Any appsettings.json key can be overridden by an environment variable, with __ as the level separator, e.g. Policy__MaxReActSteps=30 |
| Logs | docker compose logs -f next-chats (also written to the logs volume) |
| Health | The image has a built-in HEALTHCHECK on /health |
| Runs as | non-root user ($APP_UID) |
Optional — demo MCP server (Streamable HTTP, handy for a first try):
docker compose --profile demo up -d
# Admin UI → MCP servers → add http://mcp-demo:5300/mcpOptional — agent workspaces on the host: uncomment the /workspaces volume in docker-compose.yml, then create a workspace in the admin UI whose root path is /workspaces/<name>.
Upgrade / backup:
git pull && docker compose up --build -d # named volumes are kept
# backup the SQLite volume (the volume name is prefixed with the compose project name)
docker run --rm -v next-chats_next-chats-data:/d -v "$PWD":/b busybox tar czf /b/nextchats-data.tgz -C /d .The compose file does not terminate TLS. For public access put a reverse proxy in front (allow
PUT/DELETE, forwardAuthorization, and setproxy_buffering offfor SSE).
Symptom: Admin-side PUT / DELETE calls (enable/disable, edit, delete MCP servers, ...) all return 405 Method Not Allowed, while GET / POST work.
Cause: IIS's WebDAV module intercepts the PUT / DELETE verbs and answers 405 itself, so the request never reaches the app (an app response would be 401 / 404).
Fix (either):
- Use the shipped
src/NextChats.Api/web.config(dotnet publishcopies it into the publish output); it removes theWebDAVModulemodule and theWebDAVhandler. For an already deployed site, replace the publishedweb.configwith this template (keepprocessPath/arguments) and recycle the app pool — no code change needed. - Or uninstall / disable WebDAV on the server: IIS Manager → server level → Modules → remove
WebDAVModule; Handler Mappings → removeWebDAV.
Self-check: an unauthenticated PUT to any admin endpoint should return 401; 405 means WebDAV (or a reverse proxy that does not allow the verb) is still intercepting it.
If a reverse proxy such as nginx sits in front, allow PUT / DELETE (and forward the Authorization header and set proxy_buffering off for SSE as needed).
Browser automation (Playwright MCP): the MCP server spawns a headed browser in the session of its own process. Run the API standalone (dotnet NextChats.Api.dll ...) from the logged-on user's desktop session so the window is visible and login-protected sites can be signed in manually. Under IIS (w3wp) the API lives in Session 0, whose GUI is isolated from the desktop — the browser still works, but its window is never visible to the user.
When any browser tool is available, the server injects a built-in browser workflow guide into the system prompt: it opens the URL immediately (no cookies/tokens requested first), keeps the headed browser open, and when a page hits a login/SSO wall it tells the user to sign in in the already-open window and keeps polling (browser_wait_for + re-check) for up to 10 minutes in the same turn, then continues the extraction automatically — the user does not have to repeat the request. This guide is injected regardless of the selected prompt (even with "no prompt").
- default(system accounts): local password accounts (PBKDF2-SHA256), created by an admin in User Management; the login page defaults to
default. - Internal auth (acs / ucs…): admins maintain auth-center configs in Internal Auth Management — API URL, HTTP method, the request-body field names for the username/password (
body(application/json)), success-response rules (one or more fields that must be not empty or equal a fixed value, dotted paths such asdata.sessionIDsupported), and the category's default roles (multi-select). - The login page shows a sign-in-method radio group (
default+ enabled auth centers, fromGET /api/auth/providers); choosing an internal method makes the backend validate the credentials against the configured auth center. - After a successful validation the user is auto-provisioned or reused: internal users are unique by
(AuthType, Username)(adefaultand anacsaccount may share a username); when the user already exists their roles are read directly, otherwise a user is created (no password, displayName = username, status Active, bound to the provider's default roles). - Internal-auth users share the same JWT mechanism as default accounts: chat / catalog / permissions behave identically afterwards (role → model/MCP/Prompt/Skill bindings decide the visible scope).
- Dual-token session (double-token): login issues a short-lived access token (30 min) plus a persisted refresh token (7 days, stored as SHA-256 hash). When the access token expires, the frontend silently exchanges the refresh token (
POST /api/auth/refresh) and replays the request — active users are never logged out mid-work, idle sessions expire after 7 days. Every refresh rotates the token (the old one is revoked and recorded as replaced, so replays are rejected). Re-login, logout, and user disable revoke that user's refresh tokens immediately (a disabled account can no longer renew, and its access token dies within 30 min).
sequenceDiagram
participant U as User (Browser)
participant C as AuthController
participant P as InternalAuthProvider config
participant A as Auth Center (external)
participant DB as Users table
U->>C: POST /api/auth/login { authType: "acs", username, password }
C->>P: load provider (api / fields / success rules / default roles)
C->>A: HTTP POST api { username_field: …, password_field: … }
A-->>C: response JSON
C->>C: evaluate success rules (field not empty / equals value)
alt rule satisfied
C->>DB: find user by (AuthType=acs, Username)
alt not exists
C->>DB: auto-create (no password, default roles)
end
C-->>U: JWT (same mechanism as default accounts) + user info
else rule failed / auth center error
C-->>U: 401 (AUTH_INVALID_CREDENTIALS / AUTH_PROVIDER_ERROR)
end
- Effective tool intersection:
role binding ∩ user selection ∩ MCP global enabledper user, with data isolation. - Active Skill matching: Skills are exposed to the model as meta tools (
skill_<slug>); instructions are lazy-loaded to avoid token explosion. - Prompt building: a template engine (
{{var}}/#if/#each/#section) renders system prompts. - ReAct loop (AgentLoopEngine): producer–consumer Channel architecture decouples LLM streaming from the main loop; safe interruption.
- Workspace coding sessions — completion standard +
AGENTS.md: when a session is bound to a workspace, the system prompt gets two extra blocks.-
Completion standard (only at WorkspaceWrite level or above): after changing files the model must build/test via
ws_exec, fix failures and re-run, never weaken tests to make them pass, and end with a short "Verification" section listing what ran, the result, and what could not be verified. Read-only sessions don't get it (they cannot run anything). -
Project guide: the workspace root's
AGENTS.md(if present) is injected as-is, so each workspace declares its own build/test/verification commands, e.g.# AGENTS.md - build: dotnet build -c Release - test: dotnet test - frontend: cd web && npm run build
Capped at 12,000 characters (longer files are truncated); read through the workspace path sandbox (a symlink/junction pointing outside the root is ignored at levels below FullAccess); a missing, empty or unreadable file is silently skipped. The guide cannot override the permission level, safety rules or approval policy.
-
This is prompt-level guidance, not enforcement: whether the model verifies still depends on the model. An engine-side "don't finish before verifying" gate is a possible later step. Only normal workspace sessions are affected (team mode is unchanged).
-
- Execution model: each team message runs 「responsible engineer drafts a solution → assistants give independent suggestions (parallel or sequential, isolated) → responsible evaluates and produces the next-round plan (early stop on consensus) → final wrap-up」, up to
maxRounds. - SSE orchestration: a single Channel decouples production/consumption; events carry
round/phase/engineerId:team_start → team_round → team_text (open segment) → team_think_delta (reasoning stream) → team_tool_start/team_tool_result (tool calls) → team_delta (text stream) → team_end → end. Interleaved parallel suggestions are routed to the right collapsible segment (Round / participant double-folding) by round+phase+engineer. - Tool loop (same lever as normal chat): engineer calls expose the same tool set as normal chat (settings-enabled MCP/skills ∩ role binding ∩ built-ins
http_fetch/mcp_*∩ session workspacews_*). Models may take multiple decision steps: thinking streams live → tool calls execute directly (no approval in team mode, like Autonomous) → results feed back intomessages→ until direct text output. Tool cards reuse the normal-chatToolCardcomponent and the same toolTrace persistence shape. - Tool-loop guard: at most 8 tool decision steps per engineer call; if a model keeps calling tools without ever emitting text (occasional with deep-reasoning models), one final tool-less call is forced so the answer is produced from the tool results — avoiding “empty solution”. Thinking accumulates across steps; text streams live.
- Stop on consensus: after consensus is detected in an eval round the loop converges early (no further rounds). Detection is dual-channel: the explicit
[CONSENSUS]marker, or natural-language consensus phrasing in the output tail ("no remaining dispute", "达成一致"…); only the tail is checked to avoid false positives from “partly agreed but still disputed”. - Failure tolerance: solution/suggestion/eval/final calls retry once on transient errors or empty replies (tokens/cost from both attempts are counted); only then a
TEAM_*_FAILis recorded, with the reason persisted as placeholder content, and the session stays usable. - Config semantics: disabling team mode allows an empty/missing engineer list (config is kept for reuse and completeness checks are skipped); strict checks (1 responsible + ≥1 assistant, unique names, model availability/role binding) apply only when enabling.
- Token / timing semantics: TTFT = real first-token latency per segment (first reasoning/text delta); total time = wall-clock sum of segments;
ReasoningTokensis a separate field (shown in the usage panel) and included in cost (output price).RoundsJsonpersiststext/thinking/toolsper segment for faithful replay rendering. - Engineer model settings: reasoning enabled (
ThinkingEnabled/EnableReasoning, effort Medium); reasoning deltas stream asteam_think_deltaand persist as collapsible blocks.
- LLM providers (enable flag / model / endpoint / encrypted api-key / temperature / context-window)
- MCP servers (Name / Endpoint / Headers JSON encrypted; "Fetch" auto-discovers tools/prompts/resources with schemas; per-item disable)
- Multiple Prompts & Skills (pluggable, lazy-load)
- RBAC: users / roles; role ↔ MCP / Prompt / Skill / LLM model bindings —— bound models are the only models the role's users can see/select, and direct API calls with an unauthorized model id are rejected server-side (
MODEL_NOT_AUTHORIZED) - Internal Auth: auth-center configs (acs / ucs…) with success-response rules and default roles (see Authentication above)
- LLM Router: multi-vendor priority + round-robin + failover (mark-unhealthy circuit breaking); OpenAI-compatible & Mock clients.
- MCP driver: strictly follows the latest MCP spec (see MCP 2026-07 revision); connection pooling; call retry with backoff. MCP errors go into the loop — tool errors are fed back to the model as tool results for retry/explanation; sessions never break because of an MCP error.
- Policy engine:
Allow → Deny → RequireApprovalwith dangerous-operation heuristics (delete_alletc.); approvals support pending/approved/rejected/expired. - Context manager: token estimate → compress (LLM summary) → truncate; never exceeds the model length limit and never interrupts the session.
trace_id(trc_...) flows through Orchestration / LLM / MCP / Audit; token accounting in/out; metrics: TTFT, tokens, cost, tool latency, approval count (/api/admin/metrics/usage).- Idempotent writes: unique (UserId, ClientMessageId) key; key chat logs persist across restarts.
- To the user: friendly message + error code (no stack / endpoint / header)
- To the model: tool errors are re-fed as tool results (retryable, explainable)
- To the logs: full context (sanitized)
- Frontend:
vue-i18nwith per-domain locale files underweb/src/i18n/locales/{en,zh}/; English is the default locale. - Backend: every user-visible message lives in one dictionary (
src/NextChats.Core/Localization/Texts.cs, en/zh) — no hard-coded UI text in code. The API localizes errors via theX-Langrequest header (orAccept-Language), so non-browser clients get the same language, e.g.curl -H "X-Lang: en"; seeded content (roles, prompts, skills, demo provider) is English. - README: this English file and README.zh.md are linked at the top.
node scripts/smoke.mjs # login → create session → SSE streaming chat → persistence → metrics
node scripts/smoke-tools.mjs # ReAct tool call (tool:echo) → dangerous approval flow (danger:delete_all)
# Internal-auth E2E (mock auth center + PowerShell suite, see e2e/README.md)
node e2e/mock-auth.js # start mock auth center (127.0.0.1:53131)
powershell -File e2e/run-internal-auth-e2e.ps1 # assert CRUD / providers / auto-provision / uniqueness / role-based catalogRemote MCP servers protected by OAuth are supported in addition to static-header HTTP servers (e.g. Coralogix https://api.ap2.coralogix.com/mgmt/api/v1/mcp):
- Admin → MCP Servers → create/edit an HTTP server and set Auth mode = OAuth 2.1 (browser authorization).
- Click Authorize: the backend discovers the protected-resource metadata (RFC 9728) and the authorization-server metadata (RFC 8414), dynamically registers a client (RFC 7591) when a
registration_endpointexists, then returns an authorization URL carrying PKCE (S256) and astatevalue; NextChats opens it in the browser. - Sign in to the provider and click Authorize; the provider redirects to
/api/admin/mcp-servers/{id}/oauth/callback, where the authorization code is exchanged for tokens (AES-GCM encrypted in SQLite) and the page reports success. McpDriverinjectsAuthorization: Bearer <access_token>automatically, refreshes it with therefresh_tokenwhen it expires (or when starting a tool call near expiry), and the Revoke button clears the stored tokens.
The callback route is anonymous (the browser follows a redirect) but validated by the OAuth state (15-minute TTL); token/config values are never returned by the API — only masked authorization status.
Redirect URI rule: for
httprequests the callback falls back tohttp://localhost:{port}(RFC 8252 native-client loopback), because providers such as Coralogix reject non-loopbackhttpredirect URIs during dynamic registration (invalid_redirect_uri). The browser therefore must run on the same machine as NextChats when authorizing;https://hosts keep the request host.
Coralogix data region: the authorization page's “Your data region” is decided by the MCP endpoint host. Use the host matching your Coralogix data region:
Region MCP endpoint EU1 https://api.eu1.coralogix.com/mgmt/api/v1/mcpEU2 https://api.eu2.coralogix.com/mgmt/api/v1/mcpAP1 https://api.ap1.coralogix.com/mgmt/api/v1/mcpAP2 https://api.ap2.coralogix.com/mgmt/api/v1/mcpAP3 https://api.ap3.coralogix.com/mgmt/api/v1/mcpUS1 https://api.us1.coralogix.com/mgmt/api/v1/mcpUS2 https://api.us2.coralogix.com/mgmt/api/v1/mcp(
api.coralogix.comcurrently resolves to EU1.) The MCP server dialog shows a Coralogix region selector that rewrites the endpoint host; changing the endpoint clears the previous OAuth registration/tokens, so authorize again.
- What's new in MCP (2026-07-28)
- ModelContextProtocol C# SDK 2.2.0:
McpClient.CreateAsync+HttpClientTransport(Streamable HTTP) /StdioClientTransport
This project is licensed under the Apache License 2.0.
