mcp-core
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-corelist all registered tools with permissions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-core
Shared MCP-server machinery for the DNS platform Flask apps (SR, Books, CRM,
DM, PM). Each app currently hand-rolls the same five things in its
app/routes/mcp.py; this library extracts them once, behind interfaces:
Module | What it extracts | Reference implementation |
| Streamable HTTP transport (spec rev 2025-03-26): single | SR |
|
| SR/Books |
|
| SR |
|
| Books |
| The frozen tool-error contract ( | both |
|
| new (was implicit log lines) |
| Catalog-driven Members + Roles admin pages (0.2.0): | Books |
App-agnostic by construction: zero imports from any app, no SQLAlchemy
dependency, no models. Every integration point is a typing.Protocol the
app implements over its own tables. Flask is the only runtime dependency
(the transport is a Blueprint); the app decides the Flask/SQLAlchemy
versions in play.
Frozen contracts
These are wire contracts the Phone app, ChatGPT connectors, and cross-app service-token callers already depend on. Changing any of them requires a coordinated migration across every consumer — treat them as frozen:
Tool errors are successful JSON-RPC results. A refused or failed tool call returns
"result"whosecontent[0].textis the JSON of{"error": "<message>"}. JSON-RPC"error"objects are protocol-level only (-32700 parse, -32601 unknown method, -32000 unauthenticated, -32603 unexpected handler exception).Frozen error strings (agents parse and act on these):
Unknown tool: {name}caller_phone or caller_email is required when calling via a service tokenPermission denied: {permission}
Result serialization: the tool result dict is serialized with
json.dumps(result, indent=2)into a singletextcontent block.Endpoint semantics:
POST <prefix>(and<prefix>/) for JSON-RPC;GET-> 405 withAllow: POST, DELETE;DELETE-> 200 no-op; missing or invalid bearer -> 401 with JSON-RPC error code -32000.
Related MCP server: @variant/mcp-server
Fail-closed guarantees (preserved from the shipped slices)
Unknown tool name -> refused before anything else is inspected.
Service token without
caller_phone/caller_email-> refused, unless the tool is registeredidentity_exempt=True. Keep that set tiny and structural (machine-to-machine callbacks only — SR'sreceive_call_resultis the canonical member). Never exempt a tool a human invokes.Unresolvable caller (unknown identity, misconfigured staff role, no org membership, no resolver configured) -> refused. Resolvers signal this by raising
IdentityError; the message is surfaced verbatim as the tool error.Unmapped permission -> a tool declares its requirement at registration;
dynamic_permissionderivingNonefrom the arguments refuses the call. There is no allow-by-default path anywhere.No fallback tiers. An unknown caller never becomes a guest. SR-style anonymous external tiers are an explicit opt-in via
with_external_tiers(staff_resolver, classifier)— and even then the fallback is phone-keyed only; an unknowncaller_emailalways fails closed (an email must never bootstrap an identity).
Usage sketch
from mcp_core import (
AnyOf, Caller, IdentityError, McpServer, TokenContext, ToolRegistry,
hash_token, streamable_http_blueprint,
)
# 1) Bearer validation over the app's oauth_tokens table.
class AppTokenValidator:
def validate(self, bearer):
row = OAuthToken.query.filter_by(access_token_hash=hash_token(bearer)).first()
if not row or not row.is_access_valid():
return None
if row.organization_id:
return TokenContext(org_id=str(row.organization_id), kind="service")
user = User.query.get(row.user_id)
if not user or not user.is_active():
return None
return TokenContext(org_id=None, kind="user", principal_user_id=str(user.id))
# 2) Caller resolution over the app's users/memberships.
class AppCallerResolver:
def resolve(self, org_id, caller_phone=None, caller_email=None):
ou = ... # phone wins over email; search ONLY org_id; normalize app-side
if ou is None:
raise IdentityError("caller not recognized in this organization")
return Caller(user_id=str(ou.user_id), role=ou.role,
permissions=frozenset(perms_for(ou)), extra={"org_user": ou})
class AppPrincipalResolver:
def resolve_principal(self, token, org_id):
ou = ... # verify ACTIVE membership of token.principal_user_id in org_id
if ou is None:
raise IdentityError("Access denied to this organization")
return Caller(...)
# 3) Declare tools with the app's permission keys.
registry = ToolRegistry(
caller_resolver=AppCallerResolver(),
principal_resolver=AppPrincipalResolver(),
permission_evaluator=lambda caller, key: ..., # optional: has_permission semantics
)
@registry.tool("list_invoices", permission="ar.invoices.view",
schema={"type": "object", "properties": {...}})
def list_invoices(ctx, args):
# ctx.caller is the resolved acting person; ctx.org_id the tenant.
return {"invoices": [...]}
# 4) Mount the transport.
server = McpServer(registry=registry, validator=AppTokenValidator(),
name="Books MCP Server", version="1.0.0")
app.register_blueprint(streamable_http_blueprint(server)) # serves POST /mcp0.1.1 additions (SR pilot)
ToolRegistry(visibility_filter=...): optional app-ownedtools/listview —(token, org_id) -> iterable of visible tool names | None. Returning names advertises exactly those registered tools (registration order);Nonefalls through to the default logic. Advertisement-only; never gates dispatch. Built for apps that migrate withpermission=Noneregistrations (per-handler gates kept in place, e.g. SR): their per-caller listing can't be derived from declared requirements.mcp_core.transport.dispatch_message(server, token, data)is now public: legacy transports still mounted during a migration (SR's/mcp/ssePOST) route their JSON-RPC through the same single dispatch core. The caller owns bearer validation and the HTTP envelope.
0.2.0 additions — mcp_core.authz_ui (catalog-driven admin UI)
New subpackage: shared Members + Roles admin pages generated from a
per-app permission catalog, so all five apps get one consistent roles
matrix instead of five hand-built ones. Same construction rules as the
rest of the library — no database access, no app imports, every
integration point a protocol — but it is admin-UI machinery, not
MCP-server machinery, so it is not re-exported from the top-level
mcp_core namespace. Import from mcp_core.authz_ui directly.
1) Declare a catalog
The catalog is the single source of truth: ordered groups (module ->
entries), each entry a dotted key (module.entity.action[.scope], 2–4
lowercase segments) plus display label and optional description. Keys are
the SAME strings the app's web gates and MCP tool registrations use.
Validation (well-formed keys, uniqueness, non-empty labels/groups) runs at
construction, so a bad catalog fails at app startup, never in front of an
admin.
Books adopts its existing PERMISSION_LABELS mapping verbatim:
from mcp_core.authz_ui import PermissionCatalog
from app.utils.permissions import PERMISSION_LABELS, PERMISSION_INHERITANCE
catalog = PermissionCatalog.from_dict(
PERMISSION_LABELS, inheritance=PERMISSION_INHERITANCE
)Apps without a labels dict declare groups directly:
catalog = PermissionCatalog(
[
("Properties", [
("pm.properties.view", "View properties"),
("pm.properties.manage", "Manage properties",
"Create, edit, archive properties"),
]),
("Users & Permissions", [
("settings.users.view", "View team members"),
("settings.users.invite", "Invite new users"),
("settings.roles.manage", "Manage roles (create/edit/delete)"),
]),
],
inheritance={"manage": ["view"]}, # display hint only (data-implies +
) # a note); enforcement stays app-side2) Implement the provider
AuthzProvider is the app-side seam — the package never touches a DB.
Implement it over the app's own tables (Books: organization_users +
roles + role_permissions):
from mcp_core.authz_ui import AuthzError, Member, Role
class AppAuthzProvider:
def can_manage(self, actor): # the single page gate; fail closed
ou = current_org_user()
return ou is not None and (
ou.role in ("owner", "admin")
or has_permission(ou, "settings.users.edit")
)
def list_members(self, org): ... # -> [Member(...), ...]
def get_member(self, org, member_id): ...
def invite_member(self, org, email, role_id): ... # app owns email/token flow
def set_member_role(self, org, member_id, role_id): ...
def deactivate_member(self, org, member_id): ...
def reactivate_member(self, org, member_id): ...
def list_roles(self, org): ... # -> [Role(...), ...]
def get_role(self, org, role_id): ... # None -> 404
def create_role(self, org, name, description=""): ...
def set_role_permissions(self, org, role_id, keys): ... # FULL-set replace
def delete_role(self, org, role_id): ...Conventions: org is opaque (whatever your org_loader returns — id, ORM
row, or None for single-tenant). Refuse an operation by raising
AuthzError("user-facing message") — HTML pages flash it, JSON endpoints
return it as a 400 {"error": ...}. Mark hardcoded roles (Books'
owner/admin) Role(is_system=True): they render as "Full access /
built-in", are excluded from the invite form, and the blueprint refuses to
edit or delete them even on a forged form (keep the provider refusing too
— defense in depth). Every matrix save is validated against the catalog
before the provider sees it, so unknown/forged keys can never be
written.
3) Mount the blueprint
from mcp_core.authz_ui import authz_admin_blueprint
app.register_blueprint(authz_admin_blueprint(
catalog,
AppAuthzProvider(),
base_template="base.html", # must define a `content` block
url_prefix="/admin/authz", # default
actor_loader=lambda: current_user, # optional; default passes None
org_loader=lambda: session.get("org_id"), # optional; default passes None
))Routes (all gated by provider.can_manage(actor) -> 403; JSON routes get
a JSON 403 body):
Route | What it does |
| Member table, invite form, per-row role assign + activate toggle |
| Member ops (form posts, redirect + flash) |
| Role list + create form |
| Permission matrix generated from the catalog: one fieldset per group, one checkbox per key |
| Saves the FULL checked set (unchecked = revoked) |
| Delete role |
| JSON mirror of every operation for JS-driven pages (Books' settings page fetches JSON today) — same gate, same validation |
Templates extend the app-supplied base_template and fill its content
block. Markup is plain semantic HTML with Bootstrap-ish class names
(table, btn btn-primary, badge) plus authz-* hooks — styled apps
look native, unstyled apps degrade fine. Inheritance hints render as
data-implies="..." attributes on checkboxes for apps that want Books'
auto-check-view JS behavior.
Adoption: like the MCP migration, each app swaps its existing Users & Permissions pages in its own dedicated slice — nothing imports
authz_uiyet. PM goes first: it has NO roles UI at all today, so it adopts greenfield (declare catalog, implement provider, mount) with nothing to swap. Books is the reference UX this generalizes (app/templates/modules/settings.html); it can keep its JS-driven page on the/api/*endpoints and retire its hand-built matrix last.
Adoption guide (per app)
Each app migrates in its own slice, in four steps:
Implement
TokenValidatorover the app'soauth_tokenstable (mcp_core.hash_tokenmatches the existingOAuthToken.hash_tokensha256 digests byte for byte, so no data migration). Service tokens map toTokenContext(kind="service", org_id=...); user tokens toTokenContext(kind="user", principal_user_id=...).Implement
CallerResolver(andPrincipalResolver) over the app's users/memberships tables. Books-style: any miss raisesIdentityError(no guest tier). SR-style external callers: wrap the staff resolver withwith_external_tiers(...)and classifyexternal_known/external_newin the app. Keep resolution stateless across requests; per-request memoization (Flaskg) is fine.Declare tools via
@registry.tool(name, permission=..., schema=...)using the app's existing permission keys (the same strings the web UI enforces). Port Books'TOOL_PERMISSIONSentries 1:1 — tuples becomeAllOf(...),()becomesAllOf(), per-entity re-gates becomedynamic_permission. Pass the app'shas_permissionsemantics aspermission_evaluatorwhere owner-full-access / manage->view inheritance matter. Mark structural machine-to-machine callbacksidentity_exempt=True— nothing else.Mount the blueprint: build an
McpServerand registerstreamable_http_blueprint(server)at/mcp. Existing legacy routes (/mcp/sse,/mcp/messages, REST discovery endpoints) can coexist during the migration window and be retired once clients move.
Migration status: SR, Books, CRM, DM, and PM all keep their current hand-rolled MCP servers until each is migrated in its own dedicated slice. Nothing imports
mcp_coreyet; this repo only has to stay byte-compatible with the frozen contracts above so those slices are drop-in.
Development
python -m pip install -e .[dev]
python -m pytestThe test suite is pure library — fake validators/resolvers, no database, no app imports.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
MCP server for progressive tool usage at any scale (see https://klavis.ai)
OAuth-protected, read-only-by-default MCP server for provenance-labeled QuillCaddie project memory.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceFramework for building and running MCP servers as HTTP services. Define tools as pure Python functions, wire up with two lines, run with one command.-
- FlicenseNot gradedqualityBmaintenanceShared MCP HTTP server infrastructure for plugin projects, providing Express + Streamable HTTP transport, OAuth/OIDC auth, runtime configuration, tool registration, and widget support.5 npm-
- AlicenseNot gradedqualityCmaintenanceA universal MCP server for registering internal, external, and OpenAPI-based APIs as MCP tools. It exposes them to MCP clients via Streamable HTTP and provides admin portal, RBAC/session auth, credential injection, and audit logging.Academic Free v1.1
- AlicenseNot gradedqualityBmaintenanceProvides a reusable MCP server foundation with explicit tool registration, operation-mode separation, scope-based permissions, structured results, and built-in health/status tools.MIT