Hi,
This already exists. The ABP CLI ships an official MCP server that serves the ABP documentation (plus community articles, answered support questions and the framework source code) to any MCP client.
Update the CLI first, then print the client configuration:
dotnet tool update -g Volo.Abp.Studio.Cli
abp login <your-username>
abp mcp get-config
abp mcp get-config outputs this:
{
"mcpServers": {
"abp": {
"command": "abp",
"args": [
"mcp"
],
"env": {}
}
}
}
For Claude Code, drop that JSON into .mcp.json in your solution folder (or ~/.claude.json for all projects). For Codex, the same server goes into ~/.codex/config.toml:
[mcp_servers.abp]
command = "abp"
args = ["mcp"]
The server runs over stdio through the CLI, so it picks up your ABP login automatically. An active license is required, and the tools it exposes are:
get_relevant_abp_documentation — official docsget_relevant_abp_articles — community articlesget_relevant_abp_support_questions — previously answered support questionssearch_source_code — search the framework source, optionally per version (rel-8.3, rel-9.3, rel-10.0)get_source_code_file — full contents of a file from an ABP repositorylist_abp_github_repositories — the official repositoriesThanks
Hi,
CMS Kit can be used as the backend in a modern microservice solution, but it does not currently provide a React administration UI or React public pages. The modern microservice template also does not create a CMS service automatically.
For a basic news publishing feature, Volo.CmsKit is enough. Its Blogging feature provides blogs, blog posts, media support, management/public HTTP APIs, permissions, and the built-in Draft, WaitingForReview, and Published states. These are fixed states and management endpoints, not a configurable approval workflow. If you need multi-level approval, task assignment, or approval history, you need to implement those rules on top of CMS Kit.
A practical setup is:
CmsService from ABP Studio and enable its integration with the current solution.abp install-module Volo.CmsKit
Use abp install-module Volo.CmsKit.Pro only if you also need Pro features such as newsletters, polls, FAQs, URL shorting, or page feedback. CMS Kit Pro does not add another approval workflow for blog posts.
Blogs global feature before module initialization and EF Core model creation. Confirm that the no-layer DbContext contains ConfigureCmsKit() or ConfigureCmsKitPro(), then add a migration and update the CmsService database.AbpBlobStoring connection string instead of installing Blob Storing again./api/cms-kit/{**catch-all}
/api/cms-kit-admin/{**catch-all}
/api/cms-kit-public/{**catch-all}
The /api/cms/{**catch-all} route generated for a service named CmsService does not cover these paths.
CmsService OAuth scope to both apps/react and apps/react-admin-console, then run the OpenIddict data seeder again. Assign the CMS Kit Blog Post read, create, update, and publish permissions to the corresponding editor/reviewer roles.apps/react-admin-console and the public news list/detail pages in apps/react.For public news, this list endpoint returns published posts:
GET /api/cms-kit-public/blog-posts/{blogSlug}
In ABP 10.4, do not expose the following detail endpoint directly as a published-only endpoint:
GET /api/cms-kit-public/blog-posts/{blogSlug}/{blogPostSlug}
It resolves the post by the blog and post slugs without checking for the Published status. Add a facade endpoint for your React application that verifies the status before returning the article.
The official CMS Kit Demo uses MVC / Razor Pages. There is no official React CMS Kit sample that can be added directly to this solution.
You can find the relevant documentation here:
One version note: 3.0.9 is the ABP Studio version, not the ABP Framework version. A solution created with Studio 3.0.3 used ABP 10.4.0 by default, unless its package versions were upgraded separately. Updating ABP Studio does not automatically update an existing solution.
Thanks
Hi,
Your Elephant.LanguageTexts.LanguageTextController uses the same route as the Language Management one, and that's what the AmbiguousMatchException is about. Drop the conflicting UpdateAsync from your controller and call the module endpoint.
The URL needs one fix too. The API takes the resource name and the key in separate route segments, so the key is BookPrices on its own. BookMaster::BookPrices is the combined form, and passing it as the key stores a text under that literal name that nothing looks up:
PUT /api/language-management/language-texts/BookMaster/hi/BookPrices?value=<url encoded text>
With a non-empty value that creates the text if it isn't there yet.
Swagger calls the API from the same origin, so that response wasn't a CORS problem. Keep the App:CorsOrigins entry for the case where your UI runs on a different origin than the API.
Thanks
Hi,
This turned out to be a framework issue, not something in your code. Saving a BLOB also updated the container row, because a foreign key only relation was treated as a navigation change of the aggregate root. Every upload and delete wrote that single container row, so parallel uploads into the same container collided on it.
The fix is in https://github.com/abpframework/abp/pull/25937 and will be available with 10.7. The IgnoredUpdateAggregateRootSelectors configuration from the previous answer is safe to keep until you upgrade, and you can drop it after that.
We've refunded your ticket.
Thanks
Hi,
The permission system has no channel dimension and PermissionGrant has no grant/deny state, so the channel part is yours to model. The enforcement hook is the framework's though: a permission value provider returning PermissionGrantResult.Prohibited. On a single permission check any Prohibited beats the user and role providers no matter where the provider sits in the pipeline, and the same provider covers [Authorize], IPermissionChecker and the permission list behind /api/abp/application-configuration.
Deriving the channel from client_id rather than a header is the right call. OpenIddict sets client_id when it builds the access token principal, so it's there for authorization_code and password tokens alike and it's already on the validated principal — no claims contributor needed. Against a scope: a client chooses which of its allowed scopes to request, while the validated token ties the request back to the client registration that obtained it.
Read it from context.Principal, not ICurrentPrincipalAccessor. IPermissionChecker.IsGrantedAsync(ClaimsPrincipal, ...) lets a caller pass a principal that isn't the ambient one, and only context.Principal follows that.
One thing that will bite you on 8.3: register the provider with Insert(0, ...), not Add<T>().
Configure<AbpPermissionOptions>(options =>
{
options.ValueProviders.Insert(0, typeof(ChannelBlockPermissionValueProvider));
});
The batch overload of IPermissionChecker keeps the first non-Undefined result per permission on 8.3, so a provider sitting after U/R never gets to prohibit what they already granted. The single-permission path still blocks, which makes this easy to miss: the direct API call returns 403 while /api/abp/application-configuration keeps listing the permission as granted. In a tiered solution that matters — the MVC layer runs RemotePermissionChecker and takes every decision from that payload, so the block isn't enforced there at all. This changed in 9.3.7 and 10.0.1 (https://github.com/abpframework/abp/pull/24283); from those versions Prohibited wins regardless of position.
The provider:
public class ChannelBlockPermissionValueProvider : PermissionValueProvider
{
public const string ProviderName = "ChannelBlock";
public override string Name => ProviderName;
protected ICurrentTenant CurrentTenant { get; }
public ChannelBlockPermissionValueProvider(IPermissionStore permissionStore, ICurrentTenant currentTenant)
: base(permissionStore)
{
CurrentTenant = currentTenant;
}
public static string BlockKey(string channel, string subjectType, string subjectId)
{
return $"{channel}|{subjectType}|{subjectId}";
}
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
// No user: client credentials, background jobs, event handlers.
// This model only subtracts from user and role grants.
return PermissionGrantResult.Undefined;
}
var channel = ResolveChannelOrNull(context.Principal);
if (channel == null)
{
return PermissionGrantResult.Prohibited;
}
if (await IsBlockedAsync(context.Permission.Name, BlockKey(channel, "U", userId)))
{
return PermissionGrantResult.Prohibited;
}
foreach (var role in context.Principal.FindAll(AbpClaimTypes.Role).Select(x => x.Value))
{
if (await IsBlockedAsync(context.Permission.Name, BlockKey(channel, "R", role)))
{
return PermissionGrantResult.Prohibited;
}
}
return PermissionGrantResult.Undefined;
}
protected virtual async Task<bool> IsBlockedAsync(string permissionName, string key)
{
if (await PermissionStore.IsGrantedAsync(permissionName, ProviderName, key))
{
return true;
}
if (CurrentTenant.Id == null)
{
return false;
}
// Host rules are invisible from inside a tenant otherwise.
using (CurrentTenant.Change(null))
{
return await PermissionStore.IsGrantedAsync(permissionName, ProviderName, key);
}
}
// Override the PermissionValuesCheckContext overload the same way - that is the one
// /api/abp/application-configuration goes through, and it is easy to patch one and
// forget the other.
}
Precedence falls out of this: user rule, role rule, tenant row and host row are all unioned, and nothing grants anything back — a tenant admin cannot clear a host rule.
Three things around the provider. Register it in the host that serves your APIs, not in a shared layer your auth server also loads — the auth server signs users in with its own cookie and that principal has no client_id, so every permission check running under it would hit the deny branch. If any of your permissions are declared with WithProviders(...), add ChannelBlock to that list or the check skips this provider for them. And ProviderKey is capped at 64 characters, so keep channel codes short, especially if you key on role names.
ResolveChannelOrNull reads AbpClaimTypes.ClientId and looks it up in your own client-to-channel table. A table rather than a switch: many clients to one channel is the shape you want, and an admin can add a partner integration or a USSD gateway without a deployment. Expose the resolved channel through a small ambient service of your own rather than burying it in the provider — you'll want the same value for audit entries, rate limits and transaction limits. It doesn't belong in ICurrentTenant.
For storage you don't need a new mechanism. PermissionGrant is keyed on provider name, provider key and permission name, and a row is only ever selected when a provider asks for that provider name — so ChannelBlock / Mobile|U|<user-id> is invisible to U, R and C. Derive a PermissionManagementProvider with the same name and you can write those rows through IPermissionManager; the entity change drops the matching PermissionStore cache entry, so a rule takes effect on the API side without a restart. One management provider covers every channel because the channel lives in the key, so a channel added at runtime needs no new registration.
For the admin screen, map a policy first or IPermissionAppService refuses the provider outright:
Configure<PermissionManagementOptions>(options =>
{
options.ManagementProviders.Add<ChannelBlockPermissionManagementProvider>();
options.ProviderPolicies[ChannelBlockPermissionValueProvider.ProviderName] = "YourApp.ChannelBlocks.Manage";
});
After that the app service reads and writes against your provider name and key unchanged, so the built-in permission management modal renders and saves the rules. It is still one provider/key per open, so this is one view per channel driven from your own entry point rather than a real third dimension — and a checked box means "granted" there while it means "blocked" here, so relabel it before an operator sees it.
Auditing is off for these rows by default: PermissionGrant carries no audit properties and AbpAuditingOptions.EntityHistorySelectors is empty, so add a selector for PermissionGrant and you get property-level old/new values under the audit log entry, with its operator, timestamp and correlation id. What you won't get is the business reason or approval behind a rule, so record that yourself.
Last one, and it matters more than it looks for a tiered setup. The MVC layer caches the whole permission list under ApplicationConfiguration_{userId}_{culture} for ApplicationConfigurationDtoCacheAbsoluteExpiration, 300 seconds by default. Changing a rule flips the API side immediately, but the MVC layer keeps serving the old answer until that entry expires. Drop the cache entry for the affected user when a rule changes if you need it to take effect at once. And because the generated hosts share one KeyPrefix, two web applications on different channels pointed at the same Redis will read each other's entry — override MvcCachedApplicationConfigurationClient.CreateCacheKey() to put the channel in the key.
Thanks
Hi,
Thanks for the code, that was enough to track it down.
Your controller is fine, and the deletes aren't part of it. What breaks is uploading into the same BLOB container in parallel: saving a blob also touches the container row in AbpBlobContainers, because ABP marks an aggregate root as updated when a related row is inserted or deleted. That gives every upload an extra UPDATE ... SET ConcurrencyStamp = @new WHERE Id = @id AND ConcurrencyStamp = @old on the single container row, so with parallel uploads the first one wins and the rest fail with that message. Uploading one by one never overlaps, which is why it works.
Add this to ConfigureServices of your module, no change to your upload code:
using Volo.Abp;
using Volo.Abp.BlobStoring.Database;
using Volo.Abp.Domain.Entities.Events;
Configure<AbpEntityChangeOptions>(options =>
{
options.IgnoredUpdateAggregateRootSelectors.Add(
new NamedTypeSelector(
"DatabaseBlobContainer",
type => type == typeof(DatabaseBlobContainer)));
});
That keeps the container row out of the aggregate-root update, and parallel uploads go through. Don't turn UpdateAggregateRootWhenNavigationChanges off globally, other modules rely on it. We'll handle this on the framework side so the container row is no longer written on every blob save.
For the deletion event on your React UI: the server already pushes DeleteMessage with the deleted message id to the target user's clients, so you can handle it on the hub connection you already use for ReceiveMessage:
connection.on('DeleteMessage', messageId => {
// remove the message from your local state
});
DeleteConversation is pushed the same way when a conversation is deleted.
Thanks
Hi,
The browser error is a CORS failure on http://localhost:44347. As long as that's in the way, your code never sees the actual response of the call.
Make sure that origin is in App:CorsOrigins of the API host on https://localhost:44371 (append it, don't replace what's already there), then restart that host:
"App": {
"CorsOrigins": "https://*.MyProjectName.com,http://localhost:44347"
}
For localhost the scheme and the port have to match exactly, so http://localhost:44347 and https://localhost:44347 count as two different origins.
For the 500, call the same endpoint outside the browser: from the Swagger UI of the API host after clicking Authorize, or from Postman with the same access token. AbpAccount / de-DE / AccessDenied works on a standard application, so if it still fails there, send us the full exception and stack trace from the API host log along with your ABP version. This one explains how to get the logs if you need it: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
Thanks
Hi,
There are two separate issues here.
Attachments: the Chat module doesn't provide an attachment API. Its HTTP APIs are all under api/chat/... (conversation, contact, settings), while POST /api/app/chat-attachment/upload is an endpoint in your own application.
The response you get is ABP's default client message for AbpDbConcurrencyException, meaning an optimistic concurrency check failed while saving. It doesn't say which record was involved, and the message text is the same for every entity, so we need a bit more to pin it down:
ChatAttachmentAppServiceIBlobContainer directly, and which BLOB provider is configuredWith those we can point at the exact entity or repository call that conflicts. Until then, re-uploading the files one by one keeps it working, as you already found.
Message deletion event: the server already sends one. Deleting a message pushes a DeleteMessage event carrying the deleted message id to the target user's SignalR clients through IRealTimeChatMessageSender, and deleting a conversation pushes DeleteConversation. The MVC and Blazor clients handle both.
So no custom server-side event is needed. The Angular client is where the gap is: it registers ReceiveMessage only, so an already-open conversation on the other side does not remove the deleted message in real time. Let us know your UI type and we'll follow up on the Angular side.
Thanks
Hi,
There is no separate create endpoint. To create a new key under an existing localization resource, call the same update endpoint with a non-empty value:
PUT /api/language-management/language-texts/{resourceName}/{cultureName}/{name}?value=Your%20Text
On a raw HTTP request value is a query-string parameter; with a generated Angular or C# proxy it's just the last argument of update / UpdateAsync. An empty value won't create a brand-new key.
The caller needs LanguageManagement.LanguageTexts and its LanguageManagement.LanguageTexts.Edit child permission, and the LanguageManagement.Enable feature has to be on.
A few things to watch out for:
resourceName has to be an already registered localization resource. The endpoint creates new keys, not new resources.Thanks
Hi,
ABP 10.1.1 targets Angular 21, so Angular 22 isn't a supported combination with this ABP version.
To use Angular 22, upgrade your solution to ABP 10.6 and use the package versions aligned with that release. Avoid mixing Angular 21 and Angular 22 packages in the same workspace.
For the Angular 22 migration details, see: https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6-angular-22
Thanks