Give AI agents safe, governed access to your systems.
Agents are only as useful as the systems they can reach, and every connection is a security question. Promptise MCP exposes your tools over the open Model Context Protocol with authentication, permissions, rate limits and an audit trail from the first request.
Expose your tools once, to every MCP-compatible agent.
Promptise gives you both sides of the wire: a server framework where one decorator brings authentication, authorization, rate limiting and audit logs, and a native client that discovers tools from any MCP server and hands them to your agent.
Raw tools on the network
You expose bare MCP tools with no auth, no rate limiting and no audit trail. Any agent that can reach the server can call anything on it.
One decorator, a real server
@server.tool() builds the schema from your type hints. Auth, guards, rate limits, circuit breakers and audit logs stack as middleware, and every handler can receive a typed RequestContext.
What ships with it
JWT, OAuth and API key auth, per-tool role and scope guards, composable middleware, a job queue, HMAC-chained audit logs, a live dashboard, routers, OpenAPI import and three transports (stdio, HTTP, SSE).
Turn a function you already have into an agent tool.
Tools are plain async functions. The schema comes from your type hints, and the decorator wires in authentication, authorization, dependencies and middleware — no plumbing.
Typed RequestContext
The authenticated client, claims, scopes and session state, available to every handler.
ctxRouters and namespaces
Group tools behind a prefix, with shared auth and middleware.
MCPRouterDependency injection
Depends() with generator cleanup.
DependsTool versioning
Several versions side by side; clients pin or float.
versionsSession state
Per-client storage that lasts across tool calls.
sessionsfrom promptise.mcp.server import ( HasRole, HasScope, RequestContext,) @server.tool( auth=True, guards=[HasRole("analyst"), HasScope("read")], rate_limit="60/min",)async def revenue_report( ctx: RequestContext, quarter: str, year: int,) -> dict: """Get quarterly revenue data.""" return await db.query(quarter, year)from promptise.mcp.server import MCPRouter users = MCPRouter(prefix="users") @users.tool()async def search(name: str) -> list[dict]: ... server.include_router(users) # users_searchEvery call authenticated, authorized and on the record.
Authentication plugs into the infrastructure you already run. Every call goes through the same four steps, and the last one leaves an audit entry.
Authenticate
JWT bearer, API key or OAuth identifies the caller.
Authorize
Role and scope guards decide whether this caller may use this tool.
Execute
Only then does your tool run, behind rate limits and timeouts.
Audit
The call is written to an HMAC-chained log that shows tampering.
JWT bearer
JWTAuth for HS256 and AsymmetricJWTAuth for RS256 or ES256, with JWKS support and token caching.
OAuth 2.0
Client credentials grant, with an optional built-in token endpoint for development and tests.
API keys
Timing-safe comparison, with roles and metadata attached to each key.
A mistake in one layer does not become an incident.
Beyond your own guards, the HTTP transport makes a few guarantees so a mistake in one layer does not become an incident.
Identity per request
Headers, claims and request id come from the call being handled, not the request that opened the session. A caller cannot inherit another’s identity by reusing a session id.
Sessions bound to a credential
With authentication on, a session belongs to the credential that opened it. Presenting a different one does not get you in.
Host and Origin validated
A server bound to loopback rejects foreign Host or Origin headers, so a web page cannot reach it through DNS rebinding. Behind a proxy, allow the hostname explicitly.
Approval gates
Gate any tool with requires_approval. The client’s user is asked through elicitation, or a second person of the same tenant decides, with caps on how much can be parked.
Built to run in production, not just in a demo.
Resilience, observability, background work, spec import and testing. Pick one to see what it gives you.
Fail in one place, not everywhere.
Protective middleware sits in front of your tool logic. A circuit breaker stops a failing dependency from taking the rest of the server with it, timeouts bound every call, and rate limits cap what any one client can do.
- Circuit breakers
- Per-client and per-tool rate limits
- Timeouts on every call
- Response caching, in memory or Redis
from promptise.mcp.server import ( AuthMiddleware, JWTAuth, RateLimitMiddleware,) auth = AuthMiddleware(JWTAuth("secret"))limit = RateLimitMiddleware(rate_per_minute=100)server.add_middleware(auth)server.add_middleware(limit)Agents see a clean interface, never your code.
An agent never sees your code. It sees a tool name, a description and a typed parameter schema, all generated from the function you wrote.
@server.tool()async def search_users( name: str, department: str = "all",) -> list[dict]: """Find employees by name.""" return await db.search(name, department){ "name": "search_users", "description": "Find employees by name.", "inputSchema": { "type": "object", "properties": { "name": { "type": "string" }, "department": { "type": "string", "default": "all" } }, "required": ["name"] }}Start with one tool and grow without a rewrite.
The same server goes from one tool to a full surface. Each step adds a feature, not a new framework.
One tool
A single decorator and no configuration. Agents can connect straight away.
@server.tool()Ten tools
Routers group tools by domain, and a prefix keeps the names clean.
MCPRouter(prefix="users")A production server
Add auth, rate limits and a job queue as middleware around the same tools.
server.add_middleware(...)An existing API
Import an OpenAPI spec so a whole REST surface becomes tools.
OpenAPIProvider("openapi.json")| Capability | How you use it |
|---|---|
| Schema from Python type hints | @server.tool() |
| JWT, OAuth and API key auth | AuthMiddleware(JWTAuth(...)) · APIKeyAuth |
| Per-tool role and scope guards | guards=[HasRole(...), HasScope(...)] |
| Rate limiting | rate_limit="60/min" · RateLimitMiddleware |
| Circuit breakers and timeouts | CircuitBreakerMiddleware · TimeoutMiddleware |
| HMAC-chained audit log | AuditMiddleware |
| Background job queue | MCPQueue(server, max_workers=4) |
| OpenAPI to MCP import | OpenAPIProvider("openapi.json") |
| In-process testing | TestClient(server) |
| Dependency injection and routers | Depends() · MCPRouter(prefix=...) |
- Transports: stdio, HTTP and SSE, selected with a flag when you serve.
- Everything above is optional. A server with one decorated function is a complete server.
Build your first tool API in a minute.
pip install promptise, write one decorated function, then run: promptise serve my_tools:server --transport http --port 8080