How it works¶
The whole server is one module, app/server.py.
Startup¶
main() picks the spec (see Configuration), patches it in memory (_patch_missing_request_bodies, _patch_design_schema, _patch_request_bodies — no-ops where upstream is already correct), then FastMCP.from_openapi generates one tool per operation. If the spec can't be loaded, build_error_server() serves a lone startup_error tool.
Token pass-through¶
sequenceDiagram
participant C as MCP client
participant M as TokenCaptureMiddleware
participant T as FastMCP tool
participant A as InvoiceNinjaTokenAuth
participant I as InvoiceNinja
C->>M: request + Authorization header
M->>M: 401 if header missing, else store in contextvar
M->>T: call tool
T->>A: outgoing httpx request
A->>I: X-API-TOKEN + X-Requested-With: XMLHttpRequest
TokenCaptureMiddleware(pure ASGI) requiresAuthorizationon the MCP path and stashes it in a contextvar.InvoiceNinjaTokenAuth(anhttpx.Auth) reads it on the outgoing call, strips a leadingBearer, and setsX-API-TOKEN, plus the fixedX-Requested-Withheader InvoiceNinja's docs require.
OAuth¶
In oauth and both modes the server is also an authorization server (the MCP SDK's OAuthProvider, subclassed as InvoiceNinjaOAuthProvider) with an encrypted file store under /data/oauth.
sequenceDiagram
participant C as MCP client
participant M as invoiceninja-mcp
participant U as User browser
participant I as InvoiceNinja
C->>M: request without token
M-->>C: 401 + resource_metadata
C->>M: discovery and POST /register
C->>U: open /authorize
U->>M: /authorize then login form
U->>M: email, password, optional 2FA
M->>I: POST /api/v1/login
I-->>M: session token
M->>I: POST /api/v1/tokens named MCP client
I-->>M: minted token
M-->>U: redirect with code and iss
U->>C: code
C->>M: POST /token with PKCE
M-->>C: inmcp_ access token
C->>M: tool call
M->>I: X-API-TOKEN minted token
- An unauthenticated request gets
401with aresource_metadatapointer. The client discovers the endpoints and registers itself (Dynamic Client Registration). /authorizehands the user to/login, where InvoiceNinja checks the credentials. The server mints a per-client token with the session token and never calls/logout.- The redirect carries the authorization code and
iss. The client exchanges the code (PKCE) for aninmcp_access token and a refresh token. - On a tool call the server maps the access token to the minted InvoiceNinja token and sends it as
X-API-TOKEN.
resolve_auth_mode() picks token, oauth or both from the environment, and wrap_app() assembles the ASGI stack for it. In both oauth and both, IssuerFlagMiddleware advertises iss support in the metadata and UploadAuthMiddleware gates the plain-HTTP /upload/{entity}/{id} route with the same bearer. Only in both does BearerPrefixMiddleware add Bearer to raw tokens so FastMCP can parse them.