Chapter 5 Β· Build
Implementation Walkthrough
We follow a request through the code in the exact order it travels: entry point, configuration, request-scoped token, middleware, auth, Jira client, trimming, tools, errors, and telemetry.
π― What you'll be able to do
- Trace how server.py builds and mounts the FastMCP app
- Explain why the user token lives in a ContextVar, not a global
- Read the middleware security flow and its failure responses
- Describe how the Jira client and trimmer keep responses safe and small
Server entry point
app/server.pyWhat it does
Configures logging and telemetry, creates the FastMCP server, registers tools, mounts MCP into a Starlette app, adds health endpoints and middleware, and starts Uvicorn when run as a module.
βοΈ Beginners can edit
Add new health metadata or register new tool modules as your server grows.
π§ Don't edit yet
The mount path and FastMCP construction β changing them can break the /mcp endpoint and gateway behavior.
mcp = FastMCP(settings.server_name, stateless_http=True, json_response=True)
register_tools(mcp)
# Mounted at "/", so the MCP endpoint is available at /mcp
Mount("/", app=mcp.streamable_http_app())| Endpoint | Purpose |
|---|---|
/healthz | Liveness probe β the process is alive. |
/readyz | Readiness probe β ready to receive traffic. |
/mcp | The MCP endpoint agents call. |
Configuration
app/config.pyWhat it does
A Settings class reads runtime configuration from environment variables and .env via pydantic-settings. Safe defaults let the app start locally; production secrets are injected by Azure.
βοΈ Beginners can edit
Tune limits like MAX_RESPONSE_BYTES or DEFAULT_MAX_RESULTS for your environment.
π§ Don't edit yet
Don't hard-code secrets here. Secrets come from the environment or Key Vault references, never the source.
| Setting | Default | Purpose |
|---|---|---|
GATEWAY_SHARED_SECRET | empty | Enables the APIM gateway integrity check when set. |
ALLOWED_ORIGINS | Copilot Studio / Power Platform | Browser origin allow-list. |
DEFAULT_MAX_RESULTS | 25 | Default Jira search page size. |
MAX_RESULTS_CAP | 50 | Maximum page size accepted from callers. |
MAX_RESPONSE_BYTES | 90000 | Response budget after trimming. |
MAX_TEXT_CHARS | 2000 | Long-text clipping limit. |
Request-scoped token storage
app/context.pyWhat it does
Holds the current user's bearer token while a tool runs β using a ContextVar so concurrent requests from different users never overlap.
_user_token: ContextVar[str | None] = ContextVar("user_token", default=None)ContextVar gives each async request its own isolated value. The middleware resets it in a finally block so the token never persists after the request.Middleware
app/middleware.pyWhat it does
Runs before the MCP server handles a request. For protected /mcp routes it checks the origin, optionally verifies the gateway token, requires a bearer token, binds it to the request context, and resets it afterward.
π§ Don't edit yet
The order of these checks is security-critical. Read the Security chapter before changing anything here.
Origin header β handy for server-to-server and local test calls β while still enforcing the bearer token and (when configured) the gateway secret.Bearer token & Jira site resolution
app/auth/bearer.pyWhat it does
Requires a delegated bearer token, then calls Atlassian's accessible-resources endpoint to find the user's Jira site (its cloudId). Caches the result keyed by a SHA-256 hash of the token β never the raw token.
Jira Cloud API URL shape:
https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3
Site discovery call:
https://api.atlassian.com/oauth/token/accessible-resourcesSite selection rules, in order:
- If
JIRA_CLOUD_IDis configured, use it. - Else if
JIRA_SITE_URLis configured, find the matching resource. - Else prefer a resource with Jira scopes.
- Else use the first accessible resource.
Jira client
app/jira/client.pyWhat it does
An async wrapper around Jira Cloud REST calls. It requires the current user token, resolves the site, builds URLs, sends the user's bearer token to Jira, retries transient failures (HTTP 429, 5xx, network) up to three times, and converts auth failures into safe errors.
| Client method | Jira operation |
|---|---|
whoami() | GET /myself |
search_issues() | POST /search/jql |
get_issue() | GET /issue/{key} |
create_issue() | POST /issue |
add_comment() | POST /issue/{key}/comment |
transition_issue() | POST /issue/{key}/transitions |
Modern JQL search
The implementation uses token-based paging, which Atlassian is moving toward, instead of legacy offset paging:
{
"jql": "...",
"maxResults": 25,
"fields": ["summary", "status", "assignee"],
"nextPageToken": "optional"
}nextPageToken into the next jira_search call. The response's isLast tells you when to stop.Payload trimming
app/jira/trim.pyWhat it does
Protects the agent platform from oversized responses. It requests only summary, status, and assignee from Jira, converts issues into a compact model, and enforces a byte budget.
Budget enforcement strategy:
- Serialize the result and check byte size.
- If it fits, return it.
- If not, clip summaries more aggressively.
- If still too big, drop trailing issues.
- Report how many issues were omitted.
AsyncResponsePayloadTooLarge.Tool registration
Tools are registered in app/tools/__init__.py with the @mcp.tool() decorator. The function name becomes the tool name; the docstring becomes the description.
@mcp.tool()
@_guard
async def jira_get_issue(issue_key: str) -> dict:
"""Get one Jira issue by key, trimmed to id, key, status, summary, assignee, and URL."""
async with JiraClient() as jira:
return _dump(await jira.get_issue(issue_key))Error envelope
The _guard decorator turns exceptions into structured results so agents can handle them gracefully instead of seeing a broken transport:
{
"error": "forbidden",
"status": 403,
"message": "Jira rejected the delegated token or the user lacks permission."
}Telemetry & redaction
app/telemetry.pyWhat it does
Configures logging with a filter that masks bearer tokens, authorization headers, and gateway token headers. If APPLICATIONINSIGHTS_CONNECTION_STRING is set, it configures Azure Monitor export.
log.info(headers) doesn't leak a credential into your telemetry.β Concept check
The middleware sets the token in a ContextVar and resets it in a finally block. What bug could appear if you forgot the reset?
π Chapter summary
server.pybuilds the FastMCP app, registers tools, mounts it at/(so MCP is at/mcp), and adds health endpoints.- The token lives in a
ContextVarso concurrent users never overlap; middleware sets and resets it. - The Jira client retries transient failures and returns trimmed models; the trimmer enforces a byte budget.
- The error envelope and redacting logger keep failures safe.