New MCP servers & agent testing, built in
A Python framework with service providers, an Artisan CLI, an ORM with migrations, logging channels, MCP servers, and agent evals, all in one container.
uvx fastapi-startkit@latest init my-appbootstrap/application.py
from pathlib import Pathfrom fastapi_startkit import Applicationfrom fastapi_startkit.fastapi import FastAPIProviderfrom fastapi_startkit.logging import LogProviderfrom fastapi_startkit.masoniteorm import DatabaseProvider # FastAPI Startkit ships with a fully async MasoniteORM provider.# Use it for an elegant, Active Record workflow—or completely swap it# for SQLAlchemy, SQLModel, or any async database stack you prefer.app: Application = Application( base_path=Path(__file__).parent.parent, providers=[ LogProvider, FastAPIProvider, DatabaseProvider, ])Every component is an independent module with its own service provider. Register the provider to wire in its config, container bindings, and lifecycle. Leave it out and it never loads.
Application
fastapi_startkit · container
7 of 7 providers loaded · 0 skipped
from pathlib import Pathfrom fastapi_startkit import Applicationfrom fastapi_startkit.logging import LogProviderfrom fastapi_startkit.masoniteorm import DatabaseProviderfrom fastapi_startkit.fastapi import FastAPIProviderfrom fastapi_startkit.redis import RedisProviderfrom fastapi_startkit.storage import StorageProviderfrom fastapi_startkit.ai import AIProviderfrom fastapi_startkit.mcp import McpProvider app = Application( base_path=Path(__file__).parent.parent, providers=[ LogProvider, DatabaseProvider, FastAPIProvider, RedisProvider, StorageProvider, AIProvider, McpProvider, ],)Independent packages
Each component has its own code and config, with no hard dependency on the others.
Connected by providers
A provider registers the component in the app container. Nothing else connects them.
Pay only for what you load
Skip a provider and its stack stays out of the process: a worker without FastAPIProvider never imports FastAPI.
Each module is a service provider you register. Use the whole stack or only the core. Modules you don’t register never load.
Scaffold models, run migrations, seed data, and write your own commands on Cleo.
python artisan db:make:model Orderpython artisan db:migratepython artisan db:seedExpose tools, prompts and resources over MCP, and build agents on OpenAI, Google and other models through LangChain.
@provider("openai")03 / coreFastAPI, Starlette and Pydantic underneath. OpenAPI docs and type-hinted requests, end to end.
user = await User.find_or_fail(1)04 / dataActive-record models, a chainable query builder, migrations, seeders and factories.
await Order.where("status", "paid").get()05 / logsLog to the terminal, daily files, syslog, or Slack by changing config.
Logger.info("Order paid")06 / evalsFake or record agents, assert on tool calls and trajectories, and grade answers with an LLM judge.
agent.assert_tool_called("lookup_order") Subclass Tool and give it a Pydantic schema to make it an MCP tool. It is served at POST /mcp beside your HTTP routes, with the same config, models, and logs.
“Summarize yesterday’s orders”
class SummarizeOrders(Tool): name = "summarize_orders" description = "Summarize the orders for a day." def schema(self): return DayInput async def handle(self, arguments: dict) -> Response: day = arguments["date"] orders = await Order.where("date", day).get() return Response.text(f"{orders.count()} orders")Fake or record any agent, assert on the tools it called, and grade answers with an LLM judge, all in the test runner you already use. Cassettes keep CI fast and offline.
.fake([...]).record()assert_tool_called()assert_response_judged()from unittest import IsolatedAsyncioTestCase
from app.agents import SupportAgent
class TestSupportAgent(IsolatedAsyncioTestCase):
async def test_refund_request(self):
with SupportAgent.record("refund.json") as agent:
await agent.prompt("Refund order #1042")
agent.assert_tool_called("lookup_order")
await agent.assert_response_judged(
expectation="Confirms the refund politely",
)uv run pytest tests/test_support_agent.py✓ 1 passedThe same concepts and names, in Python. Pick a concept to compare.
class Order extends Model
{
protected $fillable = ['status'];
}
Order::where('status', 'paid')->get();class Order(Model):
__table__ = "orders"
id: int
status: str
await Order.where("status", "paid").get()Eloquent → Masonite ORM: $fillable becomes typed attributes
uvx fastapi-startkit@latest init my-appuv run python artisan db:make:model Orderuv run python artisan db:migrate uvx fastapi-startkit@latest init my-app