import asyncio
import logging
import os
import sys
import time
# Configure logging to ensure it shows up in Cloud Run logs immediately
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
stream=sys.stdout,
)
logger = logging.getLogger("kumiho.fastapi.boot")
logger.info("Initializing Kumiho SaaS API...")
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app import __version__
version = "1.0.2" # CI requires a string literal assignment to 'version'
from app.rate_limit import RateLimitMiddleware
from app.correlation import CorrelationIdMiddleware
from app.errors import register_error_handlers
from app.observability import RequestLogMiddleware
from app.otel import setup_otel
from app.dependencies import ensure_tenant_context
# Core API routers (SDK-mirroring)
from app.core import (
projects_router,
spaces_router,
items_router,
revisions_router,
artifacts_router,
edges_router,
graph_router,
tenant_router,
resolve_router,
bundles_router,
attributes_router,
events_router,
mcp_router,
)
# Apps (domain-specific applications)
from app.apps.blog import router as blog_router
from kumiho.mcp_server import create_mcp_server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
[docs]
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan for Kumiho SaaS API."""
# Initialize MCP Manager
mcp_server = create_mcp_server()
mcp_manager = StreamableHTTPSessionManager(mcp_server, stateless=True)
# Start MCP Manager in background
async with mcp_manager.run():
app.state.mcp_manager = mcp_manager
# Warm control plane cache in background
async def _warm():
try:
await asyncio.sleep(5)
if os.getenv("KUMIHO_SERVICE_TOKEN"):
await asyncio.get_event_loop().run_in_executor(None, ensure_tenant_context)
logger.info("Tenant context warmed successfully.")
except Exception as exc:
logger.warning(f"Failed to warm tenant context: {exc}")
warm_task = asyncio.create_task(_warm())
yield
warm_task.cancel()
app = FastAPI(
title="Kumiho SaaS API",
description="Multi-tenant SaaS API for Kumiho - Build applications with version-controlled asset management",
version=__version__,
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan
)
# Ensure every request/response has a correlation id header.
app.add_middleware(CorrelationIdMiddleware)
# Basic access logs with correlation id.
app.add_middleware(RequestLogMiddleware)
# Standardize error responses across the API.
register_error_handlers(app)
# OpenTelemetry instrumentation (optional).
setup_otel(app, service_name="kumiho-fastapi", service_version=__version__)
# Configure CORS - environment aware
# In production, set CORS_ORIGINS environment variable to specific allowed origins
# Example: CORS_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
allowed_origins_env = os.getenv("CORS_ORIGINS", "")
allowed_origins_regex = os.getenv("CORS_ORIGIN_REGEX", "").strip()
if allowed_origins_env:
# Production: use specified origins
origins = [origin.strip() for origin in allowed_origins_env.split(",")]
else:
# Development: allow common dev origins
origins = [
"http://localhost:3000", # local web app
"http://localhost:8080", # gRPC kumiho-server
"http://localhost:5678", # n8n localhost
"https://kumiho.io", # official kumiho site
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origin_regex=allowed_origins_regex or None,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# Add rate limiting - configurable via environment
# Default: 120 requests per minute per IP
# Set RATE_LIMIT_PER_MINUTE=0 to disable rate limiting
rate_limit = int(os.getenv("RATE_LIMIT_PER_MINUTE", "120"))
if rate_limit > 0:
app.add_middleware(RateLimitMiddleware, requests_per_minute=rate_limit)
# ============================================================================
# Core API routers - SDK-mirroring CRUD endpoints
# These expose the same operations as kumiho-python Client
# ============================================================================
app.include_router(projects_router, prefix="/api/v1/projects", tags=["projects"])
app.include_router(spaces_router, prefix="/api/v1/spaces", tags=["spaces"])
app.include_router(items_router, prefix="/api/v1/items", tags=["items"])
app.include_router(revisions_router, prefix="/api/v1/revisions", tags=["revisions"])
app.include_router(artifacts_router, prefix="/api/v1/artifacts", tags=["artifacts"])
app.include_router(edges_router, prefix="/api/v1/edges", tags=["edges"])
app.include_router(graph_router, prefix="/api/v1/graph", tags=["graph"])
app.include_router(tenant_router, prefix="/api/v1/tenant", tags=["tenant"])
app.include_router(resolve_router, prefix="/api/v1/resolve", tags=["resolve"])
app.include_router(bundles_router, prefix="/api/v1/bundles", tags=["bundles"])
app.include_router(attributes_router, prefix="/api/v1/attributes", tags=["attributes"])
app.include_router(events_router, prefix="/api/v1/events", tags=["events"])
app.include_router(mcp_router, prefix="/api/v1/mcp", tags=["mcp"])
# ============================================================================
# Apps - Domain-specific applications built on top of core APIs
# ============================================================================
app.include_router(blog_router, prefix="/api/v1/apps/blog", tags=["blog"])
[docs]
@app.get("/healthz")
async def healthz():
"""Simple health check endpoint for Cloud Run"""
return {"status": "ok", "timestamp": time.time()}
[docs]
@app.get("/")
async def root():
"""Root endpoint with API information"""
return {
"service": "Kumiho SaaS API",
"version": "1.0.1",
"description": "Multi-tenant API for building applications with Kumiho",
"documentation": "/docs",
"authentication": {
"required_header": "X-Kumiho-Token",
"get_token": "https://kumiho.io - Sign up to get your service token"
},
"endpoints": {
"core_api": {
"prefix": "/api/v1",
"resources": [
"projects", "spaces", "items", "revisions",
"artifacts", "edges", "tenant", "resolve",
"bundles", "attributes", "events", "mcp"
]
},
"apps": {
"prefix": "/api/v1/apps",
"available": ["blog"]
}
},
"rate_limit": {
"enabled": rate_limit > 0,
"requests_per_minute": rate_limit if rate_limit > 0 else "unlimited"
}
}