MCP Server API Reference¶
Production framework for building MCP-compliant tool servers. Every middleware, auth provider, guard, transform, and utility is documented here.
Core¶
MCPServer¶
promptise.mcp.server.MCPServer
¶
Production-grade MCP server with decorator-based tool registration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Server name advertised to MCP clients. |
'promptise-server'
|
version
|
str
|
Server version string. |
'0.1.0'
|
instructions
|
str | None
|
Optional instructions sent to clients on initialisation. |
None
|
require_auth
|
bool
|
Force |
False
|
require_tenant
|
bool
|
Make tenant identity a server-wide invariant: every
tool, resource and prompt authenticates and carries a
|
False
|
hide_unauthorized_tools
|
bool
|
Filter |
False
|
cancel_grace_period
|
float
|
When a client cancels a call to a tool that
takes a |
5.0
|
middleware
property
¶
Decorator to register a middleware function.
Example::
@server.middleware
async def log_calls(ctx, call_next):
result = await call_next(ctx)
return result
add_middleware(middleware)
¶
Add a middleware to the processing chain.
The chain runs for tool calls, resource reads and prompt requests;
ctx.request_type says which.
asgi_app(transport='http', *, allowed_hosts=None, allowed_origins=None, cors=None, stateless=False)
¶
Return the server as an ASGI application, for uvicorn, gunicorn or hypercorn.
Serves the same routes as :meth:run (/mcp — or /sse and
/messages/ — plus /health and /health/ready) with the
same auth gate, token endpoint and CORS. The app's lifespan runs
the server's startup and shutdown hooks and the MCP session
manager, so the ASGI server must run lifespan events (uvicorn's
default --lifespan auto does).
Example::
# app.py
app = server.asgi_app(allowed_hosts=["mcp.example.com"])
# uvicorn app:app --host 0.0.0.0 --port 8080
# gunicorn app:app -k uvicorn.workers.UvicornWorker -w 4
The bind address belongs to the ASGI server, so Host
validation cannot follow it: the app always accepts the loopback
names, plus every value in allowed_hosts. Name the public host
your clients or proxy send, or every request through it is refused
with 421.
Several workers or replicas. Each process keeps its own MCP
sessions. A request whose mcp-session-id belongs to another
process is answered 404; Promptise clients then open a new
session, so routing that is not sticky turns into a new session on
almost every call. Route each mcp-session-id to one process
(sticky sessions), or pass stateless=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
str
|
|
'http'
|
allowed_hosts
|
list[str] | None
|
|
None
|
allowed_origins
|
list[str] | None
|
|
None
|
cors
|
Any
|
Optional |
None
|
stateless
|
bool
|
Streamable HTTP only. Serve each request without a session, so any worker or replica can answer it. Stateless servers cannot send requests back to the client: MCP elicitation and sampling (including elicitation approval gates) and per-session state are unavailable. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
An unknown or stdio transport, |
enable_token_endpoint(jwt_auth, clients, *, path='/auth/token', default_expires_in=86400)
¶
Enable a built-in token endpoint for development and testing.
This adds an /auth/token HTTP endpoint that issues JWT
tokens using the OAuth2 client_credentials flow. Clients send
{"client_id": "...", "client_secret": "..."} and receive
a signed JWT back.
For production, use a proper Identity Provider (Auth0, Keycloak, Okta, etc.) instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
jwt_auth
|
Any
|
The |
required |
clients
|
dict[str, dict[str, Any]]
|
Mapping of |
required |
path
|
str
|
HTTP path for the endpoint (default |
'/auth/token'
|
default_expires_in
|
int
|
Default token lifetime in seconds. |
86400
|
jwt_auth = JWTAuth(secret="my-secret")
server.add_middleware(AuthMiddleware(jwt_auth))
server.enable_token_endpoint(
jwt_auth=jwt_auth,
clients={
"agent-admin": {"secret": "s3cret", "roles": ["admin"]},
"agent-viewer": {"secret": "v1ewer", "roles": ["viewer"]},
},
)
exception_handler(exc_type)
¶
Register a custom exception handler.
The handler receives (ctx, exc) and should return an
MCPError instance.
Example::
@server.exception_handler(DatabaseError)
async def handle_db_error(ctx, exc):
return ToolError("DB unavailable", code="DB_ERROR", retryable=True)
include_prompts(*sources)
¶
Register Promptise prompts as MCP prompt endpoints.
Accepts any combination of:
- :class:
~promptise.prompts.registry.PromptRegistry— exposes the latest version of every registered prompt. - :class:
~promptise.prompts.core.Prompt— exposes a single prompt. - :class:
~promptise.prompts.suite.PromptSuite— exposes all prompts in the suite.
Each prompt is converted to an MCP PromptDef with arguments
extracted from the prompt's function signature. Each argument is
described by the YAML file's arguments, an
Annotated[..., Field(description=...)] hint, or else by the
placeholder it fills, its type and its default. The MCP handler
converts the string-valued arguments to the parameters' types and
calls render_async(**arguments) to produce fully rendered
prompt text with context providers, strategy, perspective, and
constraints applied. Templates written as docstrings are dedented,
so the code's indentation is not sent to the model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*sources
|
Any
|
Prompt registries, individual prompts, or suites. |
()
|
from promptise.prompts.registry import registry
server.include_prompts(registry)
include_router(router, *, prefix='', tags=None)
¶
Merge a router's registrations into this server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
router
|
Any
|
The |
required |
prefix
|
str
|
Additional prefix (combined with router's own prefix). |
''
|
tags
|
list[str] | None
|
Additional tags (combined with router's own tags). |
None
|
notify_prompts_changed()
async
¶
Send notifications/prompts/list_changed to every connected client.
Sent automatically when a prompt is registered while the server is serving.
Returns:
| Type | Description |
|---|---|
int
|
How many sessions the notification reached. |
notify_resources_changed()
async
¶
Send notifications/resources/list_changed to every connected client.
Sent automatically when a resource or resource template is registered while the server is serving.
Returns:
| Type | Description |
|---|---|
int
|
How many sessions the notification reached. |
notify_tools_changed()
async
¶
Send notifications/tools/list_changed to every connected client.
Registering a tool while the server is serving sends this
automatically; call it yourself after changing what list_tools
returns some other way.
Returns:
| Type | Description |
|---|---|
int
|
How many sessions the notification reached. |
on_shutdown(func)
¶
Register a shutdown hook.
on_startup(func)
¶
Register a startup hook.
prompt(name=None, *, description=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a function as an MCP prompt.
The handler may return a str (one user message), a
PromptMessage, a list mixing those (or {"role", "content"}
dicts), or a full GetPromptResult. MCP sends arguments as
strings; they are coerced to the handler's type hints. Requests run
through the server's middleware chain, like tool calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Prompt name (defaults to function name). |
None
|
description
|
str | None
|
Description (defaults to docstring). |
None
|
tags
|
list[str] | None
|
Optional tags for categorisation. |
None
|
auth
|
bool
|
Require authentication to get this prompt. |
False
|
roles
|
list[str] | None
|
Required roles shorthand (implies |
None
|
guards
|
list[Any] | None
|
Access control guards, checked before the handler. |
None
|
rate_limit
|
str | None
|
Rate limit string, e.g. |
None
|
timeout
|
float | None
|
Per-request timeout in seconds. |
None
|
resource(uri, *, name=None, description=None, mime_type=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a function as an MCP resource.
Reads run through the server's middleware chain, like tool calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
Static resource URI (e.g. |
required |
name
|
str | None
|
Resource name (defaults to function name). |
None
|
description
|
str | None
|
Description (defaults to docstring). |
None
|
mime_type
|
str | None
|
MIME type of the resource content. Defaults to
|
None
|
tags
|
list[str] | None
|
Optional tags for categorisation. |
None
|
auth
|
bool
|
Require authentication to read this resource. |
False
|
roles
|
list[str] | None
|
Required roles shorthand (adds a |
None
|
guards
|
list[Any] | None
|
Access control guards, checked before the handler. |
None
|
rate_limit
|
str | None
|
Rate limit string, e.g. |
None
|
timeout
|
float | None
|
Per-read timeout in seconds (enforced by
|
None
|
resource_template(uri_template, *, name=None, description=None, mime_type=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a function as an MCP resource template.
Each {param} placeholder matches one path segment and is passed
to the handler parameter of the same name, coerced to its type hint
({page} → int). {param*} (or {+param}) matches the
rest of the URI including /, for hierarchical ids such as
docs://pages/{path*}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri_template
|
str
|
URI template with |
required |
name
|
str | None
|
Resource name. |
None
|
description
|
str | None
|
Description. |
None
|
mime_type
|
str | None
|
MIME type (see :meth: |
None
|
tags
|
list[str] | None
|
Optional tags for categorisation. |
None
|
auth
|
bool
|
Require authentication to read these resources. |
False
|
roles
|
list[str] | None
|
Required roles shorthand (implies |
None
|
guards
|
list[Any] | None
|
Access control guards, checked before the handler. |
None
|
rate_limit
|
str | None
|
Rate limit string, e.g. |
None
|
timeout
|
float | None
|
Per-read timeout in seconds. |
None
|
run(transport='stdio', *, host='127.0.0.1', port=8080, dashboard=False, cors=None, allowed_hosts=None, allowed_origins=None)
¶
Start the server (blocking).
Over HTTP/SSE the server also answers GET /health (liveness)
and GET /health/ready (readiness from a registered
:class:HealthCheck, 503 when a required check fails) for
container and Kubernetes probes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
str
|
|
'stdio'
|
host
|
str
|
Bind host for HTTP/SSE transports. Defaults to
|
'127.0.0.1'
|
port
|
int
|
Bind port for HTTP/SSE transports. |
8080
|
dashboard
|
bool
|
Enable live terminal monitoring dashboard. |
False
|
cors
|
Any
|
Optional |
None
|
allowed_hosts
|
list[str] | None
|
|
None
|
allowed_origins
|
list[str] | None
|
|
None
|
run_async(transport='stdio', *, host='127.0.0.1', port=8080, dashboard=False, cors=None, allowed_hosts=None, allowed_origins=None)
async
¶
Start the server (async).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
str
|
|
'stdio'
|
host
|
str
|
Bind host for HTTP/SSE transports. |
'127.0.0.1'
|
port
|
int
|
Bind port for HTTP/SSE transports. |
8080
|
dashboard
|
bool
|
Enable live terminal monitoring dashboard. |
False
|
cors
|
Any
|
Optional |
None
|
allowed_hosts
|
list[str] | None
|
|
None
|
allowed_origins
|
list[str] | None
|
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
tool(name=None, *, description=None, tags=None, auth=False, rate_limit=None, timeout=None, guards=None, roles=None, title=None, read_only_hint=None, destructive_hint=None, idempotent_hint=None, open_world_hint=None, max_concurrent=None, requires_approval=False, cache=True)
¶
Register a function as an MCP tool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Tool name (defaults to function name). |
None
|
description
|
str | None
|
Tool description (defaults to docstring first line). |
None
|
tags
|
list[str] | None
|
Optional tags for categorisation. |
None
|
auth
|
bool
|
Require authentication for this tool. |
False
|
rate_limit
|
str | None
|
Rate limit string, e.g. |
None
|
timeout
|
float | None
|
Per-call timeout in seconds. |
None
|
guards
|
list[Any] | None
|
Access control guards (checked before handler). A
built-in identity guard ( |
None
|
roles
|
list[str] | None
|
Required roles shorthand (creates |
None
|
title
|
str | None
|
Human-readable title (MCP annotation hint). |
None
|
read_only_hint
|
bool | None
|
Tool does not modify state (MCP annotation hint). |
None
|
destructive_hint
|
bool | None
|
Tool may perform destructive operations (MCP annotation hint). |
None
|
idempotent_hint
|
bool | None
|
Repeated calls with same args produce same result (MCP annotation hint). |
None
|
open_world_hint
|
bool | None
|
Tool may interact with external systems (MCP annotation hint). |
None
|
max_concurrent
|
int | None
|
Maximum concurrent calls for this tool. When reached, additional calls receive a retryable error. |
None
|
requires_approval
|
bool
|
Require a human approval decision before every
call to this tool, enforced server-side for any MCP
client. The server must install an
|
False
|
cache
|
bool
|
|
True
|
@server.tool()
async def search(query: str, limit: int = 10) -> list[dict]:
"""Search records."""
return await db.search(query, limit)
MCPRouter¶
promptise.mcp.server.MCPRouter
¶
Modular grouping for tools, resources, and prompts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Prepended to all tool names (e.g. |
''
|
tags
|
list[str] | None
|
Default tags merged with per-tool tags. |
None
|
auth
|
bool | None
|
If set, overrides the per-tool / per-resource / per-prompt auth flag. |
None
|
middleware
|
list[Any] | None
|
Router-level middleware (runs after server middleware) for the router's tools, resources and prompts. |
None
|
guards
|
list[Any] | None
|
Router-level guards applied to all tools, resources and prompts. |
None
|
include_prompts(*sources)
¶
Register Promptise prompts as MCP prompt endpoints on this router.
Accepts :class:~promptise.prompts.registry.PromptRegistry,
:class:~promptise.prompts.core.Prompt, or
:class:~promptise.prompts.suite.PromptSuite instances.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*sources
|
Any
|
Prompt registries, individual prompts, or suites. |
()
|
include_router(router, *, prefix='', tags=None)
¶
Nest a sub-router.
prompt(name=None, *, description=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a prompt (same signature as MCPServer.prompt()).
resource(uri, *, name=None, description=None, mime_type=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a resource (same signature as MCPServer.resource()).
resource_template(uri_template, *, name=None, description=None, mime_type=None, tags=None, auth=False, roles=None, guards=None, rate_limit=None, timeout=None)
¶
Register a resource template (same signature as MCPServer.resource_template()).
tool(name=None, *, description=None, tags=None, auth=False, rate_limit=None, timeout=None, guards=None, roles=None, title=None, read_only_hint=None, destructive_hint=None, idempotent_hint=None, open_world_hint=None, max_concurrent=None, requires_approval=False, cache=True)
¶
Register a tool (same signature as MCPServer.tool()).
ServerSettings¶
promptise.mcp.server.ServerSettings
¶
Bases: BaseSettings
Base server settings with sensible defaults.
Reads from environment variables with PROMPTISE_ prefix.
Subclass to add your own fields.
Attributes:
| Name | Type | Description |
|---|---|---|
server_name |
str
|
MCP server name. |
log_level |
str
|
Logging level ( |
timeout_default |
float
|
Default tool timeout in seconds. |
Depends¶
promptise.mcp.server.Depends(dependency, *, use_cache=True)
¶
Mark a parameter for dependency injection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dependency
|
Any
|
A callable (sync or async) or generator that provides the dependency value. |
required |
use_cache
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
Any
|
A marker that the framework detects at call time. |
mount (server composition)¶
promptise.mcp.server.mount(parent, child, *, prefix='', tags=None)
¶
Mount a child server's tools, resources, and prompts into a parent.
All tool names are prefixed with {prefix}_ if a prefix is given.
Tags from the parent are merged with child tags.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent
|
Any
|
The parent |
required |
child
|
Any
|
The child |
required |
prefix
|
str
|
Namespace prefix for tool names. |
''
|
tags
|
list[str] | None
|
Additional tags applied to all mounted tools. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of tools mounted. |
hot_reload¶
promptise.mcp.server.hot_reload(server=None, *, transport='http', host='127.0.0.1', port=8080, watch_dirs=None, poll_interval=1.0, dashboard=False, allowed_hosts=None, allowed_origins=None)
¶
Run an MCP server with automatic restart on file changes.
Watches *.py files in the specified directories (defaults to
the current working directory). When a change is detected, the
server process is restarted.
This function blocks and runs in the parent process while the server runs as a subprocess.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
Not used directly — the server is started via re-running the current script. |
None
|
transport
|
str
|
Transport type. |
'http'
|
host
|
str
|
Bind host. |
'127.0.0.1'
|
port
|
int
|
Bind port. |
8080
|
watch_dirs
|
list[str] | None
|
Directories to watch (defaults to |
None
|
poll_interval
|
float
|
Seconds between file change checks. |
1.0
|
dashboard
|
bool
|
Enable dashboard in the child process. |
False
|
allowed_hosts
|
list[str] | None
|
|
None
|
allowed_origins
|
list[str] | None
|
|
None
|
if __name__ == "__main__":
hot_reload(transport="http", port=8080)
Context¶
ClientContext¶
promptise.mcp.server.ClientContext
dataclass
¶
Structured information about the authenticated client.
Populated by :class:AuthMiddleware after successful authentication.
Handlers receive it via ctx.client — no need to dig into
ctx.state["_jwt_payload"] or ctx.state["roles"].
Attributes:
| Name | Type | Description |
|---|---|---|
client_id |
str
|
Unique client identifier (from JWT |
tenant_id |
str | None
|
Tenant / organisation the client belongs to. Populated by
:class: |
roles |
set[str]
|
Set of role strings the client holds. |
scopes |
set[str]
|
Set of OAuth2 scope strings (from JWT |
claims |
dict[str, Any]
|
Full JWT payload dict (empty for API key auth). |
issuer |
str | None
|
JWT |
audience |
str | list[str] | None
|
JWT |
subject |
str | None
|
JWT |
issued_at |
float | None
|
JWT |
expires_at |
float | None
|
JWT |
ip_address |
str | None
|
Client IP address from the transport layer, or |
user_agent |
str | None
|
|
extra |
dict[str, Any]
|
Custom metadata populated by the server's |
@server.tool(auth=True)
async def my_tool(ctx: RequestContext) -> str:
print(ctx.client.client_id) # "agent-007"
print(ctx.client.roles) # {"admin", "analyst"}
print(ctx.client.scopes) # {"read", "write"}
print(ctx.client.issuer) # "https://auth.example.com"
print(ctx.client.ip_address) # "192.168.1.42"
print(ctx.client.extra) # {"org_id": "acme", "plan": "enterprise"}
return "ok"
has_all_roles(*roles)
¶
Check if the client has all of the given roles.
has_any_role(*roles)
¶
Check if the client has at least one of the given roles.
has_any_scope(*scopes)
¶
Check if the client has at least one of the given scopes.
has_role(role)
¶
Check if the client has a specific role.
has_scope(scope)
¶
Check if the client has a specific OAuth2 scope.
RequestContext¶
promptise.mcp.server.RequestContext
dataclass
¶
Per-request context available to handlers and middleware.
Automatically created for each tool call, resource read, or prompt
request. Handlers can receive it by declaring a parameter typed as
RequestContext, or by calling :func:get_context.
Attributes:
| Name | Type | Description |
|---|---|---|
server_name |
str
|
Name of the MCPServer instance. |
tool_name |
str
|
Name of the tool/resource/prompt being invoked. |
request_id |
str
|
Unique identifier for this request. If the client
sends an |
client_id |
str | None
|
Authenticated client identifier (set by auth middleware).
Shortcut for |
client |
ClientContext
|
Structured client context with roles, scopes, claims, IP,
and custom metadata. Populated by :class: |
meta |
dict[str, Any]
|
Raw HTTP headers (lower-cased names) of the request that carries this call — per request, not per MCP session. |
state |
dict[str, Any]
|
Arbitrary per-request state (middleware can read/write).
|
request_type |
str
|
What is being invoked: |
logger |
Logger
|
Pre-configured logger scoped to this request. |
ToolResponse¶
promptise.mcp.server.ToolResponse
dataclass
¶
Response wrapper that lets handlers return content with metadata.
Handlers can return a ToolResponse instead of a plain value to
attach metadata that is logged, recorded in audit trails, and
available to middleware. The content is serialised normally;
metadata is stored on ctx.state["response_metadata"] for
downstream use (e.g. by audit or webhook middleware).
Attributes:
| Name | Type | Description |
|---|---|---|
content |
Any
|
The actual tool result (str, dict, list, etc.). |
metadata |
dict[str, Any]
|
Key-value metadata for observability and audit.
Common keys: |
@server.tool()
async def search(query: str, ctx: RequestContext) -> ToolResponse:
results = await db.search(query)
return ToolResponse(
content=results,
metadata={"source": "primary_db", "result_count": len(results)},
)
get_context¶
promptise.mcp.server.get_context()
¶
Return the current request context.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If called outside a request lifecycle. |
get_request_headers¶
promptise.mcp.server.get_request_headers()
¶
Return HTTP request headers for the current async context.
Inside a tool handler on the HTTP/SSE transports these are the headers of the request that carries the current call (not of the request that opened the session). Returns an empty dict when called outside an HTTP request (e.g. stdio transport, tests).
set_request_headers¶
promptise.mcp.server.set_request_headers(headers)
¶
Store HTTP request headers for the current async context.
Called by the transport layer (ASGI middleware) so that
call_tool can populate RequestContext.meta with
headers like Authorization.
clear_request_headers¶
promptise.mcp.server.clear_request_headers()
¶
Clear HTTP request headers after the request completes.
Authentication¶
JWTAuth¶
promptise.mcp.server.JWTAuth
¶
JWT-based authentication provider.
Validates JWT tokens from the request metadata using HMAC-SHA256. Verified tokens are cached in an LRU to avoid repeated crypto operations on the hot path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
secret
|
str
|
Shared secret for HS256 signature verification. |
required |
audience
|
str | Sequence[str] | None
|
Expected |
None
|
issuer
|
str | None
|
Expected |
None
|
meta_key
|
str
|
Key in |
'authorization'
|
cache_size
|
int
|
Max number of verified tokens to cache (0 to disable). |
256
|
auth = JWTAuth(
secret=os.environ["JWT_SECRET"],
audience="crm-mcp",
issuer="https://auth.example.com",
)
create_token(payload, *, expires_in=3600)
¶
Create a signed JWT token (utility for testing).
When this provider has an audience or issuer and payload
does not set aud / iss, the token gets the (first) expected
audience and the issuer, so it verifies against this provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict[str, Any]
|
Claims to include in the token. |
required |
expires_in
|
int
|
Token lifetime in seconds. |
3600
|
verify_token(token)
¶
Check if a token is valid without requiring a request context.
Useful for transport-level auth gating.
AsymmetricJWTAuth¶
promptise.mcp.server.AsymmetricJWTAuth
¶
JWT authentication using asymmetric algorithms (RS256, ES256).
Validates JWT tokens signed with RSA or ECDSA keys. Requires the
PyJWT and cryptography packages (optional dependencies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
public_key
|
str
|
PEM-encoded public key string, or path to a PEM file. Used for signature verification. |
required |
algorithm
|
str
|
JWT algorithm ( |
'RS256'
|
audience
|
str | Sequence[str] | None
|
Expected |
None
|
issuer
|
str | None
|
Expected |
None
|
meta_key
|
str
|
Key in |
'authorization'
|
cache_size
|
int
|
Max cached tokens (0 to disable). |
256
|
auth = AsymmetricJWTAuth(
public_key=open("public.pem").read(),
algorithm="RS256",
)
server.add_middleware(AuthMiddleware(auth))
JwksAuth¶
promptise.mcp.server.JwksAuth
¶
JWT authentication against an identity provider's JWKS endpoint.
Verifies tokens issued by an external identity provider — Microsoft
Entra, an OIDC provider, an internal IdP — by fetching the provider's
public keys from its JWKS URL and selecting the one matching the
token's kid. Keys are fetched on demand and cached, so issuer key
rotation is handled without reconfiguration.
This is the server-side counterpart to promptise.identity: an
agent presents an IdP-issued identity credential, this provider
verifies it, and the validated sub / claims are surfaced on
ctx.client so guards (RequireClientId, HasRole) and
audit logs can see which agent called.
Requires the PyJWT and cryptography packages.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
jwks_url
|
str
|
The IdP's JWKS endpoint (for Entra, e.g.
|
required |
audience
|
str
|
Expected |
required |
issuer
|
str | None
|
Expected |
None
|
algorithms
|
tuple[str, ...]
|
Accepted signing algorithms. Asymmetric only, so a
token claiming |
('RS256', 'ES256')
|
meta_key
|
str
|
Key in |
'authorization'
|
leeway
|
float
|
Seconds of clock skew tolerated when checking |
60.0
|
Keys are cached for five minutes. A token signed with a key that is not in the cache (the IdP rotated its keys) makes the server re-fetch the JWKS immediately, at most once per key id per minute, so a rotated key is accepted on its first use.
Example::
auth = JwksAuth(
jwks_url="https://idp.example.com/.well-known/jwks.json",
audience="api://my-mcp-server",
issuer="https://idp.example.com",
)
server.add_middleware(AuthMiddleware(auth))
Or let the JWKS endpoint be discovered from the issuer's OIDC
.well-known/openid-configuration (see :meth:from_discovery)::
auth = JwksAuth.from_discovery(
issuer="https://login.microsoftonline.com/<tenant>/v2.0",
audience="api://my-mcp-server",
)
authenticate(ctx)
async
¶
Authenticate by verifying a JWT against the IdP's JWKS.
from_discovery(*, issuer, audience, algorithms=('RS256', 'ES256'), meta_key='authorization', request_timeout=5.0, leeway=60.0)
classmethod
¶
Build a :class:JwksAuth that discovers its JWKS from the issuer.
Resolves the JWKS endpoint from the IdP's OIDC discovery document at
{issuer}/.well-known/openid-configuration instead of taking a
jwks_url directly. The document is fetched once (lazily, on the
first verification) and its issuer is checked against the
configured issuer to prevent a spoofed discovery host. Tokens are
then verified with their iss pinned to issuer (and aud to
audience, which is required).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
issuer
|
str
|
The OIDC issuer URL (its |
required |
audience
|
str
|
Expected |
required |
algorithms
|
tuple[str, ...]
|
Accepted signing algorithms. |
('RS256', 'ES256')
|
meta_key
|
str
|
Key in |
'authorization'
|
request_timeout
|
float
|
Seconds to wait for the discovery and JWKS fetches. |
5.0
|
leeway
|
float
|
Seconds of clock skew tolerated when checking |
60.0
|
verify_token(token)
¶
Check if a token is valid without requiring a request context.
APIKeyAuth¶
promptise.mcp.server.APIKeyAuth
¶
API key-based authentication provider.
Supports two key formats:
Simple — {api_key: client_id}::
APIKeyAuth(keys={"sk-abc": "agent-1", "sk-xyz": "agent-2"})
Rich — {api_key: {"client_id": ..., "roles": [...]}}::
APIKeyAuth(keys={
"sk-abc": {"client_id": "agent-1", "roles": ["admin", "write"]},
"sk-xyz": {"client_id": "agent-2", "roles": ["read"]},
})
Rich keys populate ctx.state["roles"] so that role-based
guards (HasRole, HasAllRoles) work out of the box.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
dict[str, str | dict[str, Any]]
|
Mapping of |
required |
header
|
str
|
HTTP header name used to transmit the key. |
'x-api-key'
|
authenticate(ctx)
async
¶
Authenticate using an API key from request metadata.
On success, sets ctx.state["roles"] if the key config
includes roles.
Returns:
| Type | Description |
|---|---|
str
|
The |
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
If the key is missing or invalid. |
verify_token(key)
¶
Check if an API key is valid without requiring a context.
AuthProvider¶
promptise.mcp.server.AuthProvider
¶
Bases: Protocol
Protocol for authentication providers.
A custom provider verifies the request's credential (ctx.meta holds
the request headers) and returns the client id. To give the caller
roles, scopes and a tenant, store the verified claims as a dict in
ctx.state["jwt_payload"]: AuthMiddleware builds ctx.client
from them as it does for a JWT (roles, scope, the tenant claim,
iss / aud / sub / iat / exp).
authenticate(ctx)
async
¶
Authenticate the request.
Returns:
| Type | Description |
|---|---|
str
|
Client identifier string on success. |
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
On authentication failure. |
AuthMiddleware¶
promptise.mcp.server.AuthMiddleware
¶
Middleware that enforces authentication on tools with auth=True.
After successful authentication, populates ctx.client with a
fully structured :class:ClientContext containing roles, scopes,
standard JWT claims, client IP, and user-agent.
Optionally accepts an on_authenticate hook for custom
enrichment (e.g. loading org, tenant, or plan info from a database).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
Any
|
An |
required |
on_authenticate
|
OnAuthenticateHook | None
|
Optional async or sync callable that receives
|
None
|
tenant_claim
|
str
|
JWT claim name carrying the client's tenant /
organisation id (default |
'tenant_id'
|
async def enrich_client(client: ClientContext, ctx: RequestContext):
org = await db.get_org_for_client(client.client_id)
client.extra["org_id"] = org.id
client.extra["plan"] = org.plan
server.add_middleware(AuthMiddleware(auth, on_authenticate=enrich_client))
authenticate(ctx)
async
¶
Verify the request's credentials and populate ctx.client.
Runs the provider and the on_authenticate hook. Used for every
tool call to an auth=True tool, and for tools/list when the
server hides tools the caller may not call.
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
Missing or invalid credentials. |
OnAuthenticateHook¶
promptise.mcp.server.OnAuthenticateHook = Callable[[ClientContext, RequestContext], Awaitable[None] | None]
module-attribute
¶
TokenEndpointConfig¶
promptise.mcp.server.TokenEndpointConfig
¶
Configuration for the built-in OAuth 2.0 token issuer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
jwt_auth
|
Any
|
The |
required |
clients
|
dict[str, dict[str, Any]]
|
Mapping of |
required |
path
|
str
|
HTTP path for the endpoint (default |
'/auth/token'
|
default_expires_in
|
int
|
Default access token lifetime in seconds. |
3600
|
refresh_token_ttl
|
int
|
Refresh token lifetime in seconds
(default 30 days). Set to |
30 * 86400
|
Guards¶
Guard¶
promptise.mcp.server.Guard
¶
Bases: Protocol
Protocol for tool access guards.
check(ctx)
async
¶
Check whether the request is allowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
RequestContext
|
The current request context. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
describe_denial(ctx)
¶
Return a human-readable explanation of why access was denied.
Guards should override this to provide helpful error messages. The default returns a generic message with the guard class name.
RequireAuth¶
promptise.mcp.server.RequireAuth
¶
HasRole¶
promptise.mcp.server.HasRole
¶
Bases: Guard
Guard that requires any of the given roles.
Reads roles from ctx.client.roles (or ctx.state["roles"]),
populated by AuthMiddleware from the JWT payload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
roles
|
str
|
One or more role names. Access granted if the client has at least one of these roles. |
()
|
HasAllRoles¶
promptise.mcp.server.HasAllRoles
¶
Bases: Guard
Guard that requires all of the given roles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
roles
|
str
|
All of these roles must be present on the client. |
()
|
HasScope¶
promptise.mcp.server.HasScope
¶
Bases: Guard
Guard that requires any of the given OAuth2 scopes.
Reads scopes from ctx.client.scopes, populated by
AuthMiddleware from the JWT scope claim (space-separated
string per RFC 8693).
.. note::
Scopes are a JWT concept. When using ``APIKeyAuth`` without
JWT, ``ctx.client.scopes`` will be empty and scope guards
will always deny. Use role-based guards (``HasRole``) for
API key auth.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scopes
|
str
|
One or more scope strings. Access granted if the client has at least one of these scopes. |
()
|
@server.tool(auth=True, guards=[HasScope("read", "admin")])
async def get_data() -> str:
return "data"
HasAllScopes¶
promptise.mcp.server.HasAllScopes
¶
Bases: Guard
Guard that requires all of the given OAuth2 scopes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scopes
|
str
|
All of these scopes must be present on the client. |
()
|
RequireClientId¶
promptise.mcp.server.RequireClientId
¶
Bases: Guard
Guard that requires a specific client_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
allowed_ids
|
str
|
Set of allowed client identifiers. |
()
|
Middleware¶
Composable middleware pipeline. Order matters: outermost first.
Middleware¶
promptise.mcp.server.Middleware
¶
Bases: Protocol
Protocol for middleware callables.
MiddlewareChain¶
promptise.mcp.server.MiddlewareChain
¶
Executes a stack of middleware around a handler.
Middleware runs in registration order (first added = outermost). The innermost function is the actual handler.
LoggingMiddleware¶
promptise.mcp.server.LoggingMiddleware
¶
Log every tool call with timing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_level
|
int
|
Logging level (default |
INFO
|
StructuredLoggingMiddleware¶
promptise.mcp.server.StructuredLoggingMiddleware
¶
Emit structured JSON log entries for every tool call.
Each log entry includes:
- event: "tool_call_start" or "tool_call_end"
- tool: Tool name
- request_id: Unique request identifier
- client_id: Authenticated client (if any)
- duration_ms: Execution time (on completion)
- status: "ok" or "error"
- error: Error message (on failure)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_level
|
int
|
Python logging level (default |
INFO
|
include_args
|
bool
|
Include tool arguments in logs (default |
False
|
TimeoutMiddleware¶
promptise.mcp.server.TimeoutMiddleware
¶
Enforce per-call timeout.
Uses the tool's timeout setting if present, otherwise falls
back to default_timeout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default_timeout
|
float
|
Default timeout in seconds. |
30.0
|
RateLimitMiddleware¶
promptise.mcp.server.RateLimitMiddleware
¶
Middleware that enforces rate limits.
Buckets are per client (and per tenant) only when ctx.client_id is
set by the time this middleware runs, which takes an AuthMiddleware
added before it and an authenticated tool (auth=True, or
MCPServer(require_auth=True)). Otherwise every caller shares one
"global" bucket; the server logs a warning at build time when an
AuthMiddleware is installed but cannot identify the caller for this
middleware. Pass key_func to choose the key yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limiter
|
Any | None
|
A |
None
|
per_tool
|
bool
|
If |
False
|
key_func
|
Callable[[RequestContext], str] | None
|
Custom function to extract the rate limit key from
context. Overrides |
None
|
rate_per_minute
|
int
|
Default rate if creating a new limiter. |
60
|
burst
|
int | None
|
Default burst if creating a new limiter. |
None
|
TokenBucketLimiter¶
promptise.mcp.server.TokenBucketLimiter
¶
Token bucket rate limiter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate_per_minute
|
float
|
Sustained rate (tokens refilled per minute). |
60
|
burst
|
int | None
|
Maximum burst size (bucket capacity). |
None
|
consume(key)
¶
Try to consume one token from the bucket for key.
CircuitBreakerMiddleware¶
promptise.mcp.server.CircuitBreakerMiddleware
¶
Circuit breaker middleware.
Tracks consecutive failures per tool. When failure_threshold
consecutive failures occur, the circuit opens and subsequent calls
are rejected immediately with :class:CircuitOpenError (a retryable
CIRCUIT_OPEN error). After recovery_timeout seconds the
circuit enters half-open state and lets exactly one probe call
through; other calls are rejected until the probe finishes. A
successful probe closes the circuit, a failed one re-opens it.
Only upstream and unexpected errors count as failures (see
:func:is_upstream_failure): a tool rejecting bad input, an access
denial, or a rate-limit / capacity refusal never pauses the tool for
everyone. Errors that don't count leave the failure streak as it is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failure_threshold
|
int
|
Consecutive failures before opening the
circuit (default |
5
|
recovery_timeout
|
float
|
Seconds to wait before probing recovery
(default |
60.0
|
excluded_tools
|
set[str] | None
|
Tool names exempt from circuit breaking. |
None
|
is_failure
|
Callable[[BaseException], bool] | None
|
|
None
|
CircuitOpenError¶
promptise.mcp.server.CircuitOpenError
¶
Bases: MCPError
Raised when a tool call is rejected by an open circuit.
Reaches the client as a retryable CIRCUIT_OPEN error with
details.retry_after_seconds, so an agent knows to wait (or use
another tool) instead of treating the tool as broken. Register
@server.exception_handler(CircuitOpenError) to customise it.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
The tool whose circuit is open. |
|
retry_after |
Seconds until the breaker lets a probe call through. |
is_upstream_failure¶
promptise.mcp.server.is_upstream_failure(exc)
¶
Default failure classifier for :class:CircuitBreakerMiddleware.
Counts:
- any exception that is not an
MCPError— a crash, a connection error, an upstream SDK raising: the tool is unhealthy; - a retryable
MCPErrorother than the caller-side ones below — e.g. aTIMEOUT, orToolError("upstream unavailable", retryable=True).
Does not count:
- a non-retryable
ToolError— the tool worked and rejected this input (ToolError("No product BAD")); - authentication / access denials, validation errors, rate-limit and
concurrency refusals, approval denials, and
CircuitOpenError.
CircuitState¶
promptise.mcp.server.CircuitState
¶
Bases: Enum
Circuit breaker states.
AuditMiddleware¶
promptise.mcp.server.AuditMiddleware
¶
HMAC-chained audit log middleware.
Writes one JSON line per tool call to log_path with optional
argument/result capture and HMAC chain integrity.
Each signed entry carries seq (its position in the chain),
prev_hash (the previous entry's hmac, or 64 zeros for the
first entry) and hmac. When log_path already holds entries,
the first entry this instance writes continues the chain from the
file's last entry and is marked "resumed": true. Run one writer
per file: two processes appending to the same file fork the chain.
A failed call has "status": "error", error ("ClassName:
message") and, when it raised an :class:MCPError, error_code:
the code the client received (ACCESS_DENIED for a role/scope/guard
denial, AUTHENTICATION_ERROR for a missing or rejected credential,
VALIDATION_ERROR, RATE_LIMIT_EXCEEDED, ...). Filter on
error_code, not the class name: guard denials are raised as
AuthenticationError too. Entries written before 1.3.5 have no
error_code and still verify.
The middleware only sees requests that reach the MCP layer. With
require_auth=True on an HTTP transport, a request whose token
fails verification is rejected with 401 before that, so it is in
the HTTP server's access log, not in this audit log.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_path
|
str | None
|
File path for audit log (JSONL format). If |
None
|
signed
|
bool
|
Enable HMAC chain (default |
True
|
hmac_secret
|
str | None
|
Secret key for HMAC chain. Resolved from (in order):
1. This parameter
2. |
None
|
include_args
|
bool
|
Log tool arguments (default |
False
|
include_result
|
bool
|
Log tool results (default |
False
|
max_memory_entries
|
int | None
|
How many entries :attr: |
10000
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
entries
property
¶
The most recent audit entries kept in memory (useful for testing).
At most max_memory_entries; the log file keeps every entry.
verify_chain()
¶
Verify the HMAC chain of the entries kept in memory.
Checks the entries this instance wrote (the most recent
max_memory_entries). To verify a log file — every entry,
across restarts, with the reason and line of the first break — use
:func:~promptise.mcp.server.verify_audit_log.
Returns:
| Type | Description |
|---|---|
bool
|
|
WebhookMiddleware¶
promptise.mcp.server.WebhookMiddleware
¶
Fire webhooks on tool call events.
Supports tool.success, tool.error, and tool.call events.
Webhooks are sent asynchronously and never block tool execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
Webhook endpoint URL. |
required |
events
|
set[str] | None
|
Set of event types to fire on (default: all). |
None
|
headers
|
dict[str, str] | None
|
Extra HTTP headers for the webhook request. |
None
|
timeout
|
float
|
HTTP timeout in seconds (default |
5.0
|
sent
property
¶
Access sent webhook payloads (useful for testing).
ConcurrencyLimiter¶
promptise.mcp.server.ConcurrencyLimiter
¶
Middleware that limits concurrent in-flight tool executions.
When the limit is reached, incoming requests receive a retryable
ConcurrencyLimitError (code CONCURRENCY_LIMIT_EXCEEDED)
instead of queueing indefinitely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_concurrent
|
int
|
Maximum number of concurrent tool calls.
|
100
|
PerToolConcurrencyLimiter¶
promptise.mcp.server.PerToolConcurrencyLimiter
¶
Middleware that enforces per-tool concurrency limits.
Reads max_concurrent from the ToolDef stored in
ctx.state["tool_def"]. When a tool has max_concurrent
set and the limit is reached, additional calls receive a
retryable ConcurrencyLimitError (code CONCURRENCY_LIMIT_EXCEEDED).
Added automatically — by the server and by TestClient — when any
tool declares max_concurrent. It is placed just outside the first
CircuitBreakerMiddleware, so a call refused for capacity never
reaches the breaker.
Semaphores are lazily created per tool name on first use.
BackgroundTasks¶
promptise.mcp.server.BackgroundTasks
¶
Collect and execute background tasks after the handler returns.
Tasks are added during handler execution and run sequentially after the response is sent. Errors are logged, never raised.
Can be injected via Depends(BackgroundTasks) in tool handlers.
pending
property
¶
Number of tasks waiting to be executed.
add(func, *args, **kwargs)
¶
Schedule a task to run after the handler returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[..., Any]
|
Sync or async callable. |
required |
args
|
Any
|
Positional arguments. |
()
|
kwargs
|
Any
|
Keyword arguments. |
{}
|
execute()
async
¶
Run all queued tasks sequentially.
Errors are logged and swallowed — they never reach the client.
Observability¶
HealthCheck¶
promptise.mcp.server.HealthCheck
¶
Health and readiness probe manager.
Tracks named health checks and exposes liveness/readiness as JSON resources.
add_check(name, check, *, required_for_ready=True)
¶
Register a health check.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Check name. |
required |
check
|
Callable[..., Any]
|
Callable returning |
required |
required_for_ready
|
bool
|
If |
True
|
liveness()
async
¶
Return liveness probe result as JSON string.
liveness_report()
¶
Liveness probe result as a dict (status is always "alive").
readiness()
async
¶
Return readiness probe result as JSON string.
readiness_report(*, include_errors=True)
async
¶
Run every check and return the readiness result as a dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
include_errors
|
bool
|
Include the message of a check that raised.
The unauthenticated HTTP probe route passes |
True
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
register_resources(server)
¶
Register health resources on an MCPServer.
Also makes this check set back the server's GET /health and
GET /health/ready HTTP routes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
The |
required |
MetricsCollector¶
promptise.mcp.server.MetricsCollector
¶
MetricsMiddleware¶
promptise.mcp.server.MetricsMiddleware
¶
Middleware that records per-tool call metrics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collector
|
MetricsCollector | None
|
A |
None
|
PrometheusMiddleware¶
promptise.mcp.server.PrometheusMiddleware
¶
Prometheus metrics middleware for MCP tool calls.
Records:
- mcp_tool_calls_total — Counter of tool calls (labels: tool, status)
- mcp_tool_duration_seconds — Histogram of call duration (labels: tool)
- mcp_tool_in_flight — Gauge of in-flight calls (labels: tool)
Raises ImportError if prometheus-client is not installed
(pip install "promptise[all]").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str
|
Metric namespace prefix (default |
'mcp'
|
registry
|
Any
|
Custom Prometheus |
None
|
get_metrics_text()
¶
Generate Prometheus text exposition format.
Returns:
| Type | Description |
|---|---|
str
|
Metrics in text/plain format for scraping. |
OTelMiddleware¶
promptise.mcp.server.OTelMiddleware
¶
OpenTelemetry tracing middleware.
Creates a span for each tool call with attributes for tool name, request ID, client ID, and error status. Also records a histogram metric for tool call duration.
Raises ImportError if opentelemetry-api is not installed
(pip install "promptise[all]"). It takes no endpoint: configure
the exporter on the tracer / meter provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
service_name
|
str
|
Service name for the tracer (default
|
'promptise-mcp-server'
|
tracer_provider
|
Any
|
Optional custom |
None
|
meter_provider
|
Any
|
Optional custom |
None
|
Dashboard¶
promptise.mcp.server.Dashboard
¶
Full-screen tabbed terminal dashboard with keyboard navigation.
Features:
- 6 tabs -- Overview, Tools, Agents, Logs, Metrics, Raw Logs
- Arrow-key navigation -- left/right to switch tabs
- Number keys -- 1-6 to jump directly
- Tab key -- cycle to next tab
- Live refresh -- every ~250ms in a daemon thread
- Raw log capture -- Python logger output displayed in tab 6
Runs its own refresh + keyboard threads (both daemon), safe alongside uvicorn.
DashboardMiddleware¶
promptise.mcp.server.DashboardMiddleware
¶
Outermost middleware that records request metrics for the dashboard.
Must be inserted as the first middleware (outermost position) to capture all errors including authentication failures.
DashboardState¶
promptise.mcp.server.DashboardState
dataclass
¶
Thread-safe shared state between middleware and dashboard renderer.
Simple counters and deque are GIL-protected for basic
operations, making this safe without explicit locks for our
single-writer (middleware) / single-reader (dashboard thread)
pattern.
record_request(log)
¶
Record a completed request (called from middleware).
Caching¶
CacheBackend¶
promptise.mcp.server.CacheBackend
¶
InMemoryCache¶
promptise.mcp.server.InMemoryCache
¶
In-process cache with TTL-based expiry and background cleanup.
Thread-safe for asyncio (single-threaded event loop).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
int
|
Maximum number of entries. When the limit is reached
the least recently used entry (the one read or written longest
ago) is evicted. |
0
|
cleanup_interval
|
float
|
Seconds between background sweeps of expired
entries. |
60.0
|
RedisCache¶
promptise.mcp.server.RedisCache
¶
Redis-backed cache implementing the CacheBackend protocol.
Uses redis.asyncio for async Redis operations. Values are
JSON-serialised for storage.
If redis is not installed, all operations raise ImportError
on first use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
Redis connection URL (e.g. |
'redis://localhost:6379/0'
|
prefix
|
str
|
Key prefix to namespace cache entries
(default |
'promptise:'
|
client
|
Any
|
Pre-configured |
None
|
CacheMiddleware¶
promptise.mcp.server.CacheMiddleware
¶
Server-wide caching middleware.
Caches tool results, resource reads and prompt results, keyed on the
server, request type, name, resource URI, arguments and — by default —
the authenticated caller, so one client never receives a result
computed for another. Note that it caches every tool that doesn't
opt out: mark tools that change data or must always be fresh with
@server.tool(cache=False) (tools annotated
destructive_hint=True are skipped automatically), or use
:func:cached on just the handlers you want cached.
A cache hit still runs the definition's guards (HasRole,
HasTenant, ...), so a cached result is never returned to a caller
the tool, resource or prompt would refuse. When it requires
authentication but the caller has not been identified yet
(CacheMiddleware added before AuthMiddleware), the request
bypasses the cache — whatever the scope — so a hit can never skip
authentication: add AuthMiddleware first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend
|
CacheBackend | None
|
Cache backend. |
None
|
ttl
|
float
|
Default TTL in seconds. |
60.0
|
scope
|
CacheScope
|
Who may share an entry: |
'client'
|
cached (decorator)¶
promptise.mcp.server.cached(ttl=60.0, *, key_func=None, backend=None, scope='client', coalesce=True)
¶
Decorator that caches tool handler results.
Entries are scoped to the calling principal by default, so a result
computed for one client is never returned to another — even when the
handler reads the caller with :func:get_context rather than taking
a ctx parameter. Injected parameters (ctx: RequestContext,
Depends(...) values, BackgroundTasks ...) are not part of the
key.
Identical calls that arrive while the first one is still computing
wait for its result instead of each calling the upstream service
(request coalescing, on by default), so a burst against a cold or
just-expired entry costs one upstream call, not one per caller.
Coalescing is per process; a RedisCache shared by several
replicas still sees one upstream call per replica.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ttl
|
float
|
Time-to-live in seconds. |
60.0
|
key_func
|
Callable[..., str] | None
|
Custom key function |
None
|
backend
|
CacheBackend | None
|
Cache backend. Defaults to the module-level
|
None
|
scope
|
CacheScope
|
Who may share an entry: |
'client'
|
coalesce
|
bool
|
Share one in-flight computation between identical
concurrent calls (default |
True
|
@server.tool()
@cached(ttl=300)
async def my_open_tickets(ctx: RequestContext) -> list[dict]:
return await db.tickets(owner=ctx.client.client_id)
@server.tool()
@cached(ttl=300, scope="shared")
async def exchange_rate(currency: str) -> float:
return await fx.rate(currency)
Job Queue¶
Background task queue with priority, retry, progress, and cancellation.
MCPQueue¶
promptise.mcp.server.MCPQueue
¶
MCP-native job queue for asynchronous background work.
Allows server authors to define long-running job types that agents submit and poll for results, rather than blocking on synchronous tool calls.
When attached to an MCPServer, the queue auto-registers 5 MCP
tools (queue_submit, queue_status, queue_result,
queue_cancel, queue_list) and hooks into the server lifecycle
for worker management.
Jobs are owned by the client and tenant that submitted them. The
status, result, cancel and list tools only expose a caller's own jobs;
a caller holding admin_role sees every job of its own tenant.
Ownership relies on authenticated tool calls — on a server without
require_auth=True, pass auth=True so the queue tools
authenticate; unauthenticated callers all share one anonymous owner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any | None
|
The MCPServer to attach to. When provided, tools and lifecycle hooks are registered immediately. |
None
|
backend
|
QueueBackend | None
|
Queue storage backend (default: InMemoryQueueBackend, which is process-local: jobs are lost on restart and not shared across replicas). |
None
|
max_workers
|
int
|
Maximum concurrent job workers. |
4
|
default_timeout
|
float
|
Default per-job timeout in seconds. |
300.0
|
result_ttl
|
float
|
How long to keep completed job results before auto-cleanup (seconds). |
3600.0
|
cleanup_interval
|
float
|
Seconds between cleanup sweeps. |
60.0
|
tool_prefix
|
str
|
Prefix for auto-registered tool names. |
'queue'
|
auth
|
bool
|
Require authentication on the queue tools (in addition to
the server's |
False
|
admin_role
|
str | None
|
Role that may see and cancel other clients' jobs
within its own tenant. |
'admin'
|
cancel_grace_period
|
float
|
Seconds a cancelled running job gets to stop
on its own (its |
0.0
|
server = MCPServer(name="analytics")
queue = MCPQueue(server, max_workers=4)
@queue.job(name="generate_report", timeout=60)
async def generate_report(department: str) -> dict:
await asyncio.sleep(10)
return {"department": department, "rows": 500}
server.run(transport="http", port=8080)
backend
property
¶
The queue storage backend.
job_types
property
¶
List registered job type names.
cancel(job_id, *, caller=None)
async
¶
Cancel a job.
A pending job is never started. A running job's
CancellationToken is set and its handler task is cancelled
(after cancel_grace_period, if set), so even a handler that
never calls cancel.check() stops; anything it returns
afterwards is discarded and the job stays cancelled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
Job identifier. |
required |
caller
|
QueueCaller | None
|
Restrict to jobs this caller may access ( |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with cancellation result. |
Raises:
| Type | Description |
|---|---|
ToolError
|
If the job is not found (or not visible to caller). |
get_result(job_id, *, caller=None)
async
¶
Get job result.
If the job is still in progress, returns current status instead of a result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
Job identifier. |
required |
caller
|
QueueCaller | None
|
Restrict to jobs this caller may access ( |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with job result or status. |
Raises:
| Type | Description |
|---|---|
ToolError
|
If the job is not found (or not visible to caller). |
job(name=None, *, timeout=None, max_retries=0, backoff_base=1.0)
¶
Register an async function as a queue job type.
The decorated function runs in background workers, not inline
with the tool call. It receives its arguments as keyword args,
and may optionally accept ProgressReporter or
CancellationToken parameters (detected by type annotation or
a Depends(...) default). Arguments are validated against the
handler signature when the job is submitted, and the resulting
schema is listed in the queue_submit tool description.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Job type name (defaults to function name). |
None
|
timeout
|
float | None
|
Per-job timeout (overrides queue default). |
None
|
max_retries
|
int
|
Max retry attempts on failure. |
0
|
backoff_base
|
float
|
Exponential backoff base in seconds. |
1.0
|
@queue.job(name="generate_report", timeout=60)
async def generate_report(department: str) -> dict:
return {"department": department, "rows": 500}
list_jobs(status=None, limit=20, *, offset=0, caller=None)
async
¶
List jobs, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
JobStatus | None
|
Optional status filter. |
None
|
limit
|
int
|
Maximum number of jobs to return. |
20
|
offset
|
int
|
Number of jobs to skip (pagination). |
0
|
caller
|
QueueCaller | None
|
Only list jobs this caller may access ( |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with the page of jobs and the total count of matching |
dict[str, Any]
|
jobs visible to caller. |
register(server)
¶
Register queue tools and lifecycle hooks on an MCPServer.
Called automatically when server is passed to the
constructor. Call manually when constructing the queue
separately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
The MCPServer instance. |
required |
register_health(health)
¶
Add queue health checks to a HealthCheck instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
health
|
Any
|
The HealthCheck to register on. |
required |
start()
async
¶
Start the worker loop and cleanup task.
status(job_id, *, caller=None)
async
¶
Get job status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
Job identifier. |
required |
caller
|
QueueCaller | None
|
Restrict to jobs this caller may access ( |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with job status information. |
Raises:
| Type | Description |
|---|---|
ToolError
|
If the job is not found (or not visible to caller). |
stop()
async
¶
Gracefully stop all workers.
Jobs still running are cancelled. Jobs waiting out a retry
backoff are put back on the queue, so a later :meth:start with
the same backend picks them up.
submit(job_type, args, *, priority=JobPriority.NORMAL, caller=None)
async
¶
Submit a job for background execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_type
|
str
|
Registered job type name. |
required |
args
|
dict[str, Any]
|
Arguments for the job handler. |
required |
priority
|
JobPriority
|
Scheduling priority. |
NORMAL
|
caller
|
QueueCaller | None
|
Identity that will own the job ( |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with job_id, status, and job_type. |
Raises:
| Type | Description |
|---|---|
ToolError
|
If the job type is not registered
( |
QueueBackend¶
promptise.mcp.server.QueueBackend
¶
Bases: Protocol
Protocol for pluggable queue storage backends.
The default implementation is InMemoryQueueBackend.
count(status=None)
async
¶
Count jobs, optionally filtered by status.
dequeue()
async
¶
Remove and return the highest-priority pending job, or None.
enqueue(job)
async
¶
Add a job to the queue.
get(job_id)
async
¶
Get a job by ID.
list_jobs(status=None, limit=50)
async
¶
List jobs, optionally filtered by status.
remove(job_id)
async
¶
Remove a job record. Returns True if found.
update(job)
async
¶
Update a job's state.
InMemoryQueueBackend¶
promptise.mcp.server.InMemoryQueueBackend
¶
In-process queue backend using asyncio primitives.
Uses asyncio.PriorityQueue for the pending queue and a dict
for job storage. Suitable for single-process deployments and testing:
jobs live in this process only, so they are lost when it restarts and
are not visible to other replicas of the server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
int
|
Maximum number of pending jobs (0 = unlimited). |
0
|
count(status=None)
async
¶
Count jobs, optionally filtered by status.
dequeue()
async
¶
Remove and return the highest-priority pending job.
enqueue(job)
async
¶
Add a job to the queue.
get(job_id)
async
¶
Get a job by ID.
list_jobs(status=None, limit=50)
async
¶
List jobs, optionally filtered by status.
remove(job_id)
async
¶
Remove a job record.
update(job)
async
¶
Update a job's state.
QueueCaller¶
promptise.mcp.server.QueueCaller
dataclass
¶
Identity a queue operation is performed for.
The queue's MCP tools build one from the request context, so each
caller only sees and controls its own jobs. Python code calling
:class:MCPQueue methods directly passes caller=None (the
default) and is not restricted.
Attributes:
| Name | Type | Description |
|---|---|---|
client_id |
str | None
|
Authenticated |
tenant_id |
str | None
|
The caller's tenant, if any. |
is_admin |
bool
|
Holds the queue's admin role — may see and cancel every job in its own tenant, not just its own jobs. |
can_access(job)
¶
Whether this caller may see or cancel job.
Tenants never cross: the admin role widens access to every client of the caller's own tenant, not to other tenants.
Streaming & Progress¶
StreamingResult¶
promptise.mcp.server.StreamingResult
¶
Collects partial results for streaming-style tool responses.
Tools that produce results incrementally can use this class to
accumulate items. The framework serialises the collected items
as a JSON array in a single TextContent response.
For true server-push streaming, the MCP protocol would need streaming result support. This class provides the collection pattern for when that lands.
Example::
result = StreamingResult()
result.add({"title": "Result 1", "score": 0.95})
result.add({"title": "Result 2", "score": 0.87})
return result # → TextContent with JSON array
ProgressReporter¶
promptise.mcp.server.ProgressReporter
¶
Report progress during long-running tool calls.
Sends MCP notifications/progress messages to the connected
client. If no progress token was provided by the client or no
session is available (e.g. in tests), calls are silently ignored.
Use via dependency injection::
@server.tool()
async def etl(progress: ProgressReporter = Depends(ProgressReporter)) -> str:
await progress.report(1, total=10, message="Step 1")
...
report(progress, *, total=None, message=None)
async
¶
Send a progress notification to the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
progress
|
float
|
Current progress value (e.g. items processed). |
required |
total
|
float | None
|
Total expected value (e.g. total items). If provided, clients can calculate a percentage. |
None
|
message
|
str | None
|
Optional human-readable status message. |
None
|
CancellationToken¶
promptise.mcp.server.CancellationToken
¶
Token for checking and responding to client cancellation requests.
The framework sets the token to cancelled state when the MCP client
sends a notifications/cancelled notification. Tool handlers can
poll this token during long-running operations.
Use via dependency injection::
@server.tool()
async def process(cancel: CancellationToken = Depends(CancellationToken)):
for item in items:
cancel.check() # raises CancelledError if cancelled
await do_work(item)
is_cancelled
property
¶
Whether cancellation has been requested.
reason
property
¶
The cancellation reason, if any.
cancel(reason=None)
¶
Mark this token as cancelled.
Called by the framework when a client cancellation is received.
check()
¶
Check if cancelled and raise CancelledError if so.
This is the primary API for handlers — call it at safe points in long-running loops.
wait(timeout=None)
async
¶
Wait until cancelled or timeout expires.
Returns True if cancelled, False if timed out.
Session State¶
Per-client key-value storage that persists across tool calls.
SessionState¶
promptise.mcp.server.SessionState
¶
Key-value state scoped to the current MCP session.
Each client session gets its own isolated state. State persists across tool calls within the same session and is automatically cleaned up when the session ends.
Use via dependency injection::
@server.tool()
async def counter(session: SessionState = Depends(SessionState)) -> int:
count = session.get("count", 0) + 1
session.set("count", count)
return count
SessionManager¶
promptise.mcp.server.SessionManager
¶
Manages per-session state stores.
Maps session IDs to SessionState instances. Thread-safe for
use across async tasks.
The framework creates one SessionManager per MCPServer
and looks up the current session by ID in the request context.
Tool Versioning and Transforms¶
VersionedToolRegistry¶
promptise.mcp.server.VersionedToolRegistry
¶
Overlay registry that manages versioned tools.
Sits alongside the standard ToolRegistry and is consulted at
list_tools time to expose versioned aliases.
Usage::
vr = VersionedToolRegistry()
vr.register("search", "1.0", tool_def_v1)
vr.register("search", "2.0", tool_def_v2)
# list_all() returns both pinned names and the latest alias
ToolTransform¶
promptise.mcp.server.ToolTransform
¶
Bases: Protocol
Protocol for tool transforms.
A transform receives a list of ToolDef objects and returns a
(potentially modified) list. Transforms are applied in order during
list_tools — each sees the output of the previous one.
NamespaceTransform¶
promptise.mcp.server.NamespaceTransform
¶
Prefix all tool names with a namespace.
Example::
transform = NamespaceTransform(prefix="myapp")
# "search" → "myapp_search"
VisibilityTransform¶
promptise.mcp.server.VisibilityTransform
¶
Hide tools based on a predicate.
Hidden tools are removed from list_tools results but remain
callable (for backwards compatibility with cached tool lists).
Example::
transform = VisibilityTransform(
hidden={"admin_delete": lambda ctx: "admin" not in ctx.state.get("roles", set())}
)
TagFilterTransform¶
promptise.mcp.server.TagFilterTransform
¶
Only expose tools that have at least one of the required tags.
Example::
transform = TagFilterTransform(required_tags={"public"})
# Only tools tagged "public" are listed
Elicitation and Sampling¶
Request structured input or LLM completions from the client mid-execution.
Elicitor¶
promptise.mcp.server.Elicitor
¶
Request structured input from the user mid-execution.
Bound to the MCP session by the framework's DI wiring. Sends an
elicitation/create request through ServerSession.elicit and
resolves the client's :class:mcp.types.ElicitResult.
ask() returns None — never raises — when the answer is not a
usable acceptance: the client declined or cancelled, did not declare
the elicitation capability (the request is rejected by the client),
the request timed out, or the transport failed. Every one of these
is logged at WARNING/INFO level so an operator can tell why a gated
call was denied instead of guessing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Default timeout in seconds for elicitation requests. |
60.0
|
ask(message, schema=None, *, timeout=None)
async
¶
Ask the user for structured input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
Human-readable prompt. |
required |
schema
|
dict[str, Any] | None
|
JSON Schema for the expected response. Defaults to an empty object schema (a bare confirmation). |
None
|
timeout
|
float | None
|
Override the default timeout, in seconds. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
The submitted form values when the user accepted, or |
dict[str, Any] | None
|
when the elicitor is unbound, the client declined or cancelled, |
dict[str, Any] | None
|
the client does not support elicitation, the request timed |
dict[str, Any] | None
|
out, or the transport failed. |
Sampler¶
promptise.mcp.server.Sampler
¶
Request LLM completions from the client.
Bound to the MCP session by the framework's DI wiring.
If the client does not support sampling, methods return
None silently.
create_message(messages, *, max_tokens=1024, model=None, system=None, temperature=None, stop_sequences=None)
async
¶
Request an LLM completion from the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[dict[str, str]]
|
List of message dicts with |
required |
max_tokens
|
int
|
Maximum tokens to generate. |
1024
|
model
|
str | None
|
Model hint (client may ignore). |
None
|
system
|
str | None
|
System prompt. |
None
|
temperature
|
float | None
|
Sampling temperature. |
None
|
stop_sequences
|
list[str] | None
|
Stop sequences. |
None
|
Returns:
| Type | Description |
|---|---|
str | None
|
Generated text, or |
OpenAPI¶
Generate MCP tools from OpenAPI specs automatically.
OpenAPIProvider¶
promptise.mcp.server.OpenAPIProvider
¶
Generate MCP tools from an OpenAPI specification.
Parses an OpenAPI 3.x (or Swagger 2.x) spec and registers one MCP tool per operation. Each tool makes an HTTP request to the target API when called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
str | dict[str, Any]
|
Either a URL to fetch the spec from, a file path, or a pre-parsed dict. |
required |
base_url
|
str | None
|
Override the server base URL from the spec. |
None
|
prefix
|
str
|
Prefix for generated tool names (e.g. |
''
|
include
|
set[str] | None
|
Only include operations matching these operation IDs. |
None
|
exclude
|
set[str] | None
|
Exclude operations matching these operation IDs. |
None
|
auth_header
|
tuple[str, str] | None
|
Default auth header for requests (e.g.
|
None
|
tags
|
list[str] | None
|
Tags to apply to all generated tools. |
None
|
parse()
¶
Parse the spec and return operations (does not register tools).
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
List of operation dicts with |
list[dict[str, Any]]
|
|
register(server)
¶
Parse the spec and register tools on the server.
Returns:
| Type | Description |
|---|---|
int
|
Number of tools registered. |
Exception Handlers¶
ExceptionHandlerRegistry¶
promptise.mcp.server.ExceptionHandlerRegistry
¶
Registry mapping exception types to handler functions.
Handlers are matched using the exception's MRO (most specific first).
A handler receives (ctx, exc) and must return an MCPError
instance, or None to fall through to the generic handler.
find_handler(exc)
¶
Find the best handler for exc by walking its MRO.
Returns None if no handler is registered for any type
in the exception's MRO.
An MCPError already serialises to a structured error, so for one
only handlers registered for an MCPError subclass (e.g.
CircuitOpenError, RateLimitError) apply — a catch-all
Exception handler does not swallow it.
handle(ctx, exc)
async
¶
Attempt to handle exc.
Returns the MCPError produced by the matched handler,
or None if no handler matched.
register(exc_type, handler)
¶
Register a handler for exc_type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exc_type
|
type[Exception]
|
The exception class to handle. |
required |
handler
|
Callable[..., Any]
|
Async callable |
required |
Errors¶
MCPError¶
promptise.mcp.server.MCPError
¶
Bases: Exception
Base class for structured MCP server errors.
Attributes:
| Name | Type | Description |
|---|---|---|
code |
Machine-readable error code (e.g. |
|
message |
Human-readable message. |
|
suggestion |
What the caller should try instead. |
|
retryable |
Whether re-trying the same call might succeed. |
|
details |
Additional structured context. |
to_text()
¶
Serialise to a JSON text block suitable for MCP TextContent.
ToolError¶
ResourceError¶
PromptError¶
AuthenticationError¶
RateLimitError¶
ConcurrencyLimitError¶
promptise.mcp.server.ConcurrencyLimitError
¶
Bases: RateLimitError
Raised when a tool (or the server) is already running its maximum number of concurrent calls.
A subclass of :class:RateLimitError so existing except
RateLimitError code keeps working, but with its own code,
CONCURRENCY_LIMIT_EXCEEDED: the caller is not over a quota, the
tool is busy, and a retry shortly afterwards is likely to succeed.
ValidationError¶
CancelledError¶
promptise.mcp.server.CancelledError
¶
Bases: Exception
Raised when a tool call is cancelled by the client.
Tool Types¶
ToolDef¶
promptise.mcp.server.ToolDef
dataclass
¶
Internal definition of a registered tool.
ResourceDef¶
promptise.mcp.server.ResourceDef
dataclass
¶
Internal definition of a registered resource or resource template.
The access-control fields mirror :class:ToolDef: a read runs through
the server middleware chain with this definition in
ctx.state["tool_def"], so AuthMiddleware, guards, rate limits
and timeouts apply to resources exactly as they do to tools.
PromptDef¶
promptise.mcp.server.PromptDef
dataclass
¶
Internal definition of a registered prompt.
The access-control fields mirror :class:ToolDef (see
:class:ResourceDef).
ToolAnnotations¶
promptise.mcp.server.ToolAnnotations
dataclass
¶
MCP tool annotations describing tool behaviour hints.
All fields are optional hints — they are not enforced by the framework but communicated to MCP clients so they can make informed decisions (e.g. skipping confirmation for read-only tools).
Attributes:
| Name | Type | Description |
|---|---|---|
title |
str | None
|
Human-readable title for the tool. |
read_only_hint |
bool | None
|
Tool does not modify any state. |
destructive_hint |
bool | None
|
Tool may perform destructive operations. |
idempotent_hint |
bool | None
|
Calling the tool multiple times with the same arguments produces the same result. |
open_world_hint |
bool | None
|
Tool may interact with external systems beyond the server's control. |
Content¶
promptise.mcp.server.Content = MCPTextContent | MCPImageContent | MCPEmbeddedResource | ImageContent
module-attribute
¶
ImageContent¶
promptise.mcp.server.ImageContent
¶
Helper for returning image content from tool handlers.
Wraps binary image data for automatic serialization to MCP
ImageContent (base64-encoded).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
Raw image bytes. |
required |
mime_type
|
str
|
MIME type (e.g. |
'image/png'
|
@server.tool()
async def chart(values: list[float]) -> ImageContent:
png = render_chart(values)
return ImageContent(data=png, mime_type="image/png")
to_mcp()
¶
Convert to an MCP ImageContent message.
TransportType¶
promptise.mcp.server.TransportType
¶
Bases: str, Enum
Supported MCP transport protocols.
CORSConfig¶
promptise.mcp.server.CORSConfig
dataclass
¶
CORS configuration for HTTP and SSE transports.
The defaults cover a browser MCP client: the preflight admits the MCP
request headers (:data:MCP_CORS_ALLOW_HEADERS) and the session id is
readable from the initialize response
(:data:MCP_CORS_EXPOSE_HEADERS). No origin is allowed until you name
one.
CORS only decides what a browser lets the page read. The server's
Origin validation is separate: pass the same origins to
allowed_origins on run(), or the server answers the browser's
requests 403.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
allow_origins
|
list[str]
|
Allowed origin URLs. Use |
list()
|
allow_methods
|
list[str]
|
Allowed HTTP methods. |
(lambda: ['GET', 'POST', 'DELETE', 'OPTIONS'])()
|
allow_headers
|
list[str]
|
Allowed request headers. When you replace the
default, keep |
(lambda: list(MCP_CORS_ALLOW_HEADERS))()
|
expose_headers
|
list[str]
|
Response headers the page may read. Keep
|
(lambda: list(MCP_CORS_EXPOSE_HEADERS))()
|
allow_credentials
|
bool
|
Whether to allow credentials (cookies, auth).
Cannot be combined with |
False
|
max_age
|
int
|
Max seconds browsers may cache preflight responses. |
600
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
server.run(
transport="http",
port=8080,
allowed_origins=["https://app.example.com"],
cors=CORSConfig(allow_origins=["https://app.example.com"]),
)
Testing¶
TestClient¶
promptise.mcp.server.TestClient
¶
__test__ = False
class-attribute
instance-attribute
¶
In-process test client for :class:MCPServer.
Exercises the full call pipeline — validation, dependency injection, guard checks, middleware chain, handler invocation, and error serialisation — without starting a transport.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
The |
required |
meta
|
dict[str, Any] | None
|
Simulated MCP request metadata (e.g.
|
required |
client = TestClient(server, meta={"authorization": "Bearer tok"})
result = await client.call_tool("search", {"query": "revenue"})
call_tool(name, arguments=None, *, headers=None)
async
¶
Call a tool through the full middleware pipeline.
Returns the same content list as the real MCP server (may include
TextContent, ImageContent, or EmbeddedResource).
MCPError sub-classes are serialised into structured error
JSON — they are not raised.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registered tool name. |
required |
arguments
|
dict[str, Any] | None
|
Tool arguments (validated against the input model). |
None
|
headers
|
dict[str, str] | None
|
Simulated HTTP headers (e.g. |
None
|
get_prompt(name, arguments=None, *, headers=None)
async
¶
Get a prompt result through the full pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registered prompt name. |
required |
arguments
|
dict[str, Any] | None
|
Prompt arguments. MCP clients send strings; they are coerced to the handler's type hints, as on the live server. |
None
|
headers
|
dict[str, str] | None
|
Simulated HTTP headers, merged with client-level meta. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the prompt is not found. |
MCPError
|
What the pipeline raised ( |
list_prompts(*, headers=None)
async
¶
List the registered prompts (filtered like :meth:list_resources).
list_resource_templates(*, headers=None)
async
¶
List the registered resource templates (filtered like :meth:list_resources).
list_resources(*, headers=None)
async
¶
List the registered static resources (including docs://manifest).
With MCPServer(hide_unauthorized_tools=True) only the resources
the client's credentials may read are listed, as on the live server.
list_tools(*, headers=None)
async
¶
List the registered tools (including annotations).
With MCPServer(hide_unauthorized_tools=True) only the tools the
client's credentials may call are listed, as on the live server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
dict[str, str] | None
|
Simulated HTTP headers, merged over the client meta. |
None
|
read_resource(uri, *, headers=None)
async
¶
Read a resource by URI.
Supports both static resources and URI templates, and runs the full
pipeline (see :meth:read_resource_contents).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
The resource URI (e.g. |
required |
headers
|
dict[str, str] | None
|
Simulated HTTP headers, merged with client-level meta. |
None
|
Returns:
| Type | Description |
|---|---|
str | bytes
|
The first content item: its text (a dict / list result arrives |
str | bytes
|
as JSON text), or the raw |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the resource is not found. |
MCPError
|
What the pipeline raised (see
:meth: |
read_resource_contents(uri, *, headers=None)
async
¶
Read a resource and return its MCP contents, as a client receives them.
Runs the full pipeline (middleware, auth, guards, coercion of
template parameters). Each item carries mimeType; text arrives
as TextResourceContents.text, bytes base64-encoded in
BlobResourceContents.blob.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
The resource URI (e.g. |
required |
headers
|
dict[str, str] | None
|
Simulated HTTP headers, merged with client-level meta. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the resource is not found. |
MCPError
|
What the pipeline raised ( |
Logging¶
ServerLogger¶
promptise.mcp.server.ServerLogger
¶
Send log messages from the server to the connected MCP client.
Messages appear on the client side as notifications/message
events. If no session is available (e.g. in tests), calls are
silently ignored.
Use via dependency injection::
@server.tool()
async def run(log: ServerLogger = Depends(ServerLogger)) -> str:
await log.info("starting")
...
alert(data, *, logger=None)
async
¶
Send an alert-level log message.
critical(data, *, logger=None)
async
¶
Send a critical-level log message.
debug(data, *, logger=None)
async
¶
Send a debug-level log message.
emergency(data, *, logger=None)
async
¶
Send an emergency-level log message.
error(data, *, logger=None)
async
¶
Send an error-level log message.
info(data, *, logger=None)
async
¶
Send an info-level log message.
notice(data, *, logger=None)
async
¶
Send a notice-level log message.
warning(data, *, logger=None)
async
¶
Send a warning-level log message.
Multi-Tenancy¶
RequireTenant¶
promptise.mcp.server.RequireTenant
¶
Bases: Guard
Guard that requires the client to belong to some tenant.
Reads ctx.client.tenant_id, populated by AuthMiddleware from
the configured JWT tenant claim (default tenant_id) or from the
API-key config dict. Fails closed: no authentication → no tenant →
denied. Auto-applied to every tool when the server is built with
MCPServer(require_tenant=True).
Example::
@server.tool(auth=True, guards=[RequireTenant()])
async def list_records() -> list:
...
HasTenant¶
promptise.mcp.server.HasTenant
¶
Bases: Guard
Guard that requires the client to belong to one of the given tenants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
str
|
One or more allowed tenant identifiers. |
()
|
@server.tool(auth=True, guards=[HasTenant("acme", "globex")])
async def partner_tool() -> str:
...
Approval Gates¶
ApprovalGateMiddleware¶
promptise.mcp.server.ApprovalGateMiddleware
¶
Enforces human approval on tools declared with requires_approval=True.
Runs in the middleware chain (install it after AuthMiddleware so
the approval request carries the verified caller identity). For gated
tools it builds an :class:ApprovalRequest, awaits the configured
handler's decision, and either lets the call proceed or raises
:class:ApprovalDeniedError. Non-gated tools pass through untouched.
Fail-closed semantics throughout: a timeout denies by default
(on_timeout="deny"), a handler crash denies, and a decision carrying
modified_arguments denies — the server-side gate cannot rewrite the
call's arguments (they are already bound), and silently ignoring a
reviewer's modification would execute something the reviewer did not
approve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
handler
|
Any
|
An :class: |
required |
timeout
|
float
|
Seconds to wait for a decision before applying
|
300.0
|
on_timeout
|
Literal['deny', 'allow']
|
|
'deny'
|
include_arguments
|
bool
|
Include the validated tool arguments in the
approval request (default |
True
|
PendingApprover¶
promptise.mcp.server.PendingApprover
¶
Blocks gated calls in a pending store until a human decides.
Independent four-eyes review: the gated call waits while a different
human — anyone holding approver_role — lists pending requests and
approves or denies them through two auto-registered, role-guarded admin
tools:
approvals_list()— pending requests with tool, arguments, caller identity, and age.approvals_decide(request_id, approve, reason)— release or deny; the reviewer'sclient_idis recorded on the decision.
The store is process-local (like the in-memory job queue); calls that time out before a decision are denied by the gate's default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
Server to auto-register the admin tools on. Pass |
None
|
approver_role
|
str
|
Role required to list/decide (default |
'approver'
|
max_pending
|
int
|
Deny immediately beyond this many waiting requests (default 100) — a full queue must not become an unbounded pile of blocked calls. |
100
|
max_pending_per_client
|
int | None
|
Deny immediately once a single caller has
this many requests waiting (default |
None
|
max_pending_per_tenant
|
int | None
|
Deny immediately once one tenant has this
many requests waiting, across all of its clients (default
|
None
|
caller_of(request_id)
¶
The client_id that triggered a pending request, or None.
client_key(request)
staticmethod
¶
The identity a request is counted under for max_pending_per_client.
Prefers the verified caller (caller_user_id), then the
client_id and tenant_id metadata the server-side gate
records, then the agent that raised the request. Returns ""
for a request carrying no identity — all such requests share one
bucket.
decide(request_id, approved, *, reviewer_id=None, reason=None)
¶
Resolve a pending request. Returns False if it is unknown
(already decided, timed out, or never existed).
pending()
¶
Summaries of requests currently awaiting a decision.
pending_for(client_key)
¶
Number of waiting requests counted under client_key.
pending_for_tenant(tenant_key)
¶
Number of waiting requests counted under tenant_key.
register_tools(server)
¶
Register the role-guarded approvals_list / approvals_decide
admin tools on server.
tenant_key(request)
staticmethod
¶
The tenant a request is counted under for max_pending_per_tenant.
The tenant_id the server-side gate records in the request
metadata; "" for a request without one — all such requests
share one bucket.
ElicitationApprover¶
promptise.mcp.server.ElicitationApprover
¶
Approves by asking the human behind the calling MCP client.
Uses MCP elicitation: the connected client is asked to confirm the call
({"approve": bool, "reason": str}). This is confirm-your-own-action
dual control — right for destructive-but-personal operations ("really
delete this?"). For independent four-eyes review, use
:class:PendingApprover instead.
Fail-closed: if the transport has no live MCP session (TestClient,
stdio clients without elicitation support), the request is denied
with a clear reason — never silently allowed.
ApprovalDeniedError¶
promptise.mcp.server.ApprovalDeniedError
¶
Bases: MCPError
Raised when a server-side approval gate denies (or times out on) a call.
Emitted by ApprovalGateMiddleware for tools declared with
requires_approval=True. Not retryable by default: retrying would
re-request the same approval a human just denied.
Rate Limiting¶
parse_rate_limit¶
promptise.mcp.server.parse_rate_limit(spec)
¶
Parse a declared rate-limit string like "100/min".
Accepted formats: "<count>/<unit>" where unit is one of
s|sec|second, m|min|minute, h|hr|hour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
str
|
The declared limit, e.g. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
int
|
to tokens-per-minute, and the bucket capacity (the declared count). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the spec is malformed, so a typo fails at tool registration instead of silently never limiting. |
DeclaredRateLimitMiddleware¶
promptise.mcp.server.DeclaredRateLimitMiddleware
¶
Enforces per-tool rate limits declared via @server.tool(rate_limit=...).
Also enforces rate_limit= on @server.resource /
@server.resource_template / @server.prompt.
Auto-inserted into the middleware chain at build time when any registered
tool declares a rate_limit — no manual wiring required (mirrors
PerToolConcurrencyLimiter). Each declaring tool gets its own token
bucket sized from its spec; buckets are keyed per client when
authentication populates ctx.client_id, falling back to a single
shared bucket for unauthenticated tools.
Coexists with a user-installed :class:RateLimitMiddleware: that one
enforces a server-wide policy, this one enforces each tool's declared
contract.