MCP server and access tokens
The Starter serves a Model Context Protocol server
at the gateway's /mcp. An agent in an editor or a chat, or a script, connects to it
and calls tools that read and change what its user can — as that user, and bounded
further by the token's scope, the organization's setting and a rate limit. The same
tool catalog also runs Blouppy's own assistant (see AI gateway).
What it does
Two ways in
| Client | Credential | How it gets one |
|---|---|---|
| Claude Code, Cursor, a script | A personal access token, Authorization: Bearer blp_pat_… | The user creates it in My account → Security and pastes it into the client |
| Claude.ai, Visual Studio Code, ChatGPT | An OAuth access token for the MCP resource | Authorization code with PKCE against the identity host, as a pre-registered client |
| Any connector that publishes a Client ID Metadata Document | The same OAuth token | Its client_id is an HTTPS URL; no registration needed |
Personal access tokens are minted by the identity host
(Blouppy.Identity/Services/PersonalAccessTokens/) and listed, created and revoked
under api/identity/personal-access-tokens/me. The rules are in
PersonalAccessTokenRules: a name of at most 100 characters, Read or Read and
write, a mandatory expiry of at most 366 days, at most 20 active tokens per user. The
value is shown once; only metadata is stored (personal_access_tokens). A token
also works on the REST API, which tells it apart by its blp_pat_ prefix.
Connectors go through OAuth: a missing or invalid token gets a 401 with
WWW-Authenticate: Bearer resource_metadata="…", which points the client to the
protected resource metadata at /.well-known/oauth-protected-resource/mcp, and from
there to the identity host. The user consents, and sees and revokes the connector in
My account → Security, beside their tokens (api/identity/mcp-connectors/me).
What an agent may do
Two scopes, blouppy.read and blouppy.write, where write implies read. Whatever
the scope, no token administers an organization: it never satisfies
Policies.Manage. On top of that, each organization sets its API & MCP access —
Off, Read or ReadWrite (the default) — in its AI settings. Off hides the
organization from every token; Read keeps a write token read-only there.
The tools
Reads:
| Tool | Returns |
|---|---|
list_organizations | The organizations the user belongs to and the token may read, with the role |
list_workspaces | The workspaces the user can open, in one organization or in every readable one |
get_workspace | A workspace and its team |
list_members | An organization's members, optionally matched by name — ids and display names, never an e-mail |
list_my_notifications | The user's notifications, newest first, with the unread count |
Writes:
| Tool | Through | With a token |
|---|---|---|
create_workspace | IWorkspacesService | Answers read-only |
rename_workspace | IWorkspacesService | Answers read-only |
add_team_member | ITeamsService | Answers read-only |
remove_team_member | ITeamsService | Answers read-only |
invite_member | IOrganizationInvitationsService | Answers read-only |
mark_notifications_read | INotificationsService | Allowed: the user's own data |
The administering writes are there for the assistant, which runs in the user's browser
session; through a token they are refused. There is no delete tool. Lists take limit
(20 by default, at most 50) and an opaque cursor, and answer next_cursor
(ToolPaging).
How it works
| Project / folder | Holds |
|---|---|
Blouppy.AgentTools | The catalog: ITool, IToolCatalog, ToolDefinition, ToolResult, ToolErrors, the tools, and the ports IToolAccess and IToolCaller. No SDK. |
Blouppy.Mcp | The MCP binding: AddMcpEndpoint(), MapMcpEndpoint(), McpToolRegistration |
Blouppy.Web/Security/ | ToolAccess, ToolCaller, the mcp / mcp-token / mcp-challenge schemes, ApiAccessRules |
Blouppy.Gateway/Mcp/ | McpRoute, the metadata route, the unauthenticated limit |
Blouppy.Identity | Token minting, connectors (Services/McpConnectors/), metadata documents (Services/ClientIdMetadataDocuments/), BlouppyApplicationManager |
The gateway forwards /mcp as a plain YARP route, not a session-carrying one: the
client's Authorization header is the credential and passes through, the Cookie
header is removed, and a request whose Origin is not the gateway's is refused
(UseSameOriginCheck). An address whose requests the API answered 401 more than 30
times in a minute is held back (McpRoute.UnauthenticatedRequestsPerIpPerMinute),
because each garbage token costs an introspection. See Gateway and session.
The API selects the mcp scheme for any request under /mcp. It forwards
authentication to mcp-token, an IntrospectedTokenHandler that asks the identity
host as blouppy_api, caches the answer in HybridCache for
ApiAccessRules.IntrospectionCacheDuration (60 s — how long a revoked token can still
work) and requires the token's audience to be the MCP resource, the gateway's URL
followed by /mcp. A personal access token carries both the API's and the MCP
resource's audiences; a connector's token only the MCP resource's. The endpoint
requires Policies.McpAccess. See Identity.
The server is ModelContextProtocol.AspNetCore over Streamable HTTP, stateless:
no session id, so any replica answers any request. It offers tools only.
McpToolRegistration maps a ToolDefinition to an MCP tool — readOnlyHint for a
read, idempotentHint from the definition, destructiveHint: false on writes — and a
ToolResult to a CallToolResult with structured content, or isError for a
refusal the model can read.
The catalog is shared with the assistant. A tool calls the same ports as a
controller — IXxxService to write, IXxxApiQueryService to read — and commits
through IUnitOfWork (ToolCommits), so a write fires the same domain events,
webhooks, notifications and audit entries. It never decides rights itself; it asks
IToolAccess:
public interface IToolAccess
{
Task<bool> CanReadAsync(OrganizationId organizationId, CancellationToken cancellationToken = default);
Task<bool> CanReadAsync(WorkspaceId workspaceId, CancellationToken cancellationToken = default);
Task<bool> CanManageAsync(OrganizationId organizationId, CancellationToken cancellationToken = default);
Task<bool> CanManageAsync(WorkspaceId workspaceId, CancellationToken cancellationToken = default);
Task<bool> CanWriteAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<OrganizationId>> GetReadableOrganizationIdsAsync(CancellationToken cancellationToken = default);
}
ToolAccess, in the Web, answers through IAuthorizationService and the same
Policies.Read and Policies.Manage the controllers use, plus the organization's API
& MCP access, so a tool is refused exactly where the same REST request would be.
IToolCatalog.GetDefinitionsAsync drops the write tools when CanWriteAsync is false,
so a read token, or a user whose organizations are all at Read, sees reads only — and
a write called anyway is still refused. A read token calling a write tool gets a
403 insufficient_scope naming both scopes (McpWriteScopeChallengeMiddleware),
so a connector can ask the user for write and retry.
Rate limits (ApiAccessRules, applied by ApiAccessRateLimiter to token requests
only, never the browser's):
| Partition | Limit |
|---|---|
| Per token | 120 requests a minute, burst of 30 |
| Per user, across tokens | 300 requests a minute |
| Per token, writes | 30 a minute |
Every MCP request is a POST, so on /mcp the write budget is spent per write tool
call instead (IToolCaller.TryTakeWrite), and a refusal is a tool result — too many
changes, try again in N seconds — rather than a 429.
Audit. A request with a token or a connector has the channel ActorChannel.Api,
which the audit log records on every entry. The catalog logs each
call with the tool, outcome, duration, user, token and organization, never the
arguments nor the result.
Configuration
| Setting | Host | Meaning |
|---|---|---|
ApiIntrospection:Secret | API and identity | The blouppy_api client's secret for introspection. Empty: every token is refused and a warning is logged at startup. The AppHost generates it as the parameter api-introspection-secret. |
BlouppyConfiguration:GatewayUrl | API | The public address; the MCP resource and the links tools return are built from it |
BlouppyConfiguration:IdentityUrl | API | The authorization server named in the protected resource metadata |
McpConnectors:PreRegistered:<Name> | Identity | ClientId, ClientName, ClientUri, LogoUri, RedirectUris, ClientSecret of a pre-registered connector |
ClientIdMetadataDocuments:AllowInsecureTransport | Identity | Accept an http://client_id. Forced off outside Development. |
ClientIdMetadataDocuments:AllowPrivateNetworkTargets | Identity | Fetch a document from a private address. Forced off outside Development. |
In production the secret is ApiIntrospection__Secret (see Deployment).
The identity host's appsettings.json declares Claude (blouppy-claude),
VisualStudioCode (blouppy-vscode) and ChatGpt (blouppy-chatgpt).
Metadata documents are fetched through the same PublicOnlyHttpHandler as webhooks
(see Webhooks), with a 5-second timeout and a 5 KiB size limit
(ClientIdMetadataDocumentRules).
Extending it
Adding a tool:
- Write an
internal sealed classimplementingIToolinBlouppy.AgentTools/<Area>/. ItsToolDefinitiongives a stablesnake_casename, a short title, a description written for a model,ToolKind.ReadorToolKind.Write, the JSON Schema of its arguments and, for a write,IsIdempotent. - Ask
IToolAccessbefore touching anything, and answer withToolErrors:NotFoundOrNotAccessible(never which of the two),ReadOnly, or the validator's refusal. ThrowToolArgumentExceptionfor a bad argument; the catalog turns it into a refusal. - Call the domain's port:
IXxxApiQueryServiceto read,IXxxServiceto write, thenToolCommits.CommitAsync, which maps a failedValidationResultback to the model's argument names or commits. Restate no rule. A person is an id and a display name, never an e-mail; a link comes fromToolLinks. - Register it in
AddToolCatalog(Blouppy.AgentTools/ServiceCollectionExtensions.cs) asAddScoped<ITool, YourTool>(). The MCP server and the assistant both pick it up. - Update the tests that list the catalog:
ServiceCollectionExtensionsTestsinBlouppy.AgentTools.UnitTests, andReadTools/WriteToolsinBlouppy.Web.IntegrationTests/Mcp/McpServerTests.cs, and add the tool's own unit tests beside the others. - For a write tool, give the assistant's confirmation a sentence in
front/application/src/app/features/assistant/assistant-proposal.ts; without one it falls back on the tool's title.
Limits
- No organization administration through a token, and no delete tool at all.
- A token acts in every organization its user belongs to that lets tokens in; it cannot be scoped to one organization or workspace.
- Revocation takes up to a minute to reach the API, the introspection cache's window.
- The rate limiter is per process: exact on one replica, looser with several. The numbers are a first guess, logged on every refusal.