Skip to content
MCP

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.py

What 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.

Python
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())
EndpointPurpose
/healthzLiveness probe β€” the process is alive.
/readyzReadiness probe β€” ready to receive traffic.
/mcpThe MCP endpoint agents call.

Configuration

app/config.py

What 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.

SettingDefaultPurpose
GATEWAY_SHARED_SECRETemptyEnables the APIM gateway integrity check when set.
ALLOWED_ORIGINSCopilot Studio / Power PlatformBrowser origin allow-list.
DEFAULT_MAX_RESULTS25Default Jira search page size.
MAX_RESULTS_CAP50Maximum page size accepted from callers.
MAX_RESPONSE_BYTES90000Response budget after trimming.
MAX_TEXT_CHARS2000Long-text clipping limit.

Request-scoped token storage

app/context.py

What it does

Holds the current user's bearer token while a tool runs β€” using a ContextVar so concurrent requests from different users never overlap.

Python
_user_token: ContextVar[str | None] = ContextVar("user_token", default=None)
Why not a normal global variable?
A normal global is shared across all requests. Under concurrency, User A's token could leak into User B's request β€” a serious cross-user data risk. A 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.py

What 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.

Every protected /mcp request passes these gates in order.
Note
The middleware allows requests with no 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.py

What 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.

Text
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-resources

Site selection rules, in order:

  1. If JIRA_CLOUD_ID is configured, use it.
  2. Else if JIRA_SITE_URL is configured, find the matching resource.
  3. Else prefer a resource with Jira scopes.
  4. Else use the first accessible resource.

Jira client

app/jira/client.py

What 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 methodJira 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

The implementation uses token-based paging, which Atlassian is moving toward, instead of legacy offset paging:

JSON
{
  "jql": "...",
  "maxResults": 25,
  "fields": ["summary", "status", "assignee"],
  "nextPageToken": "optional"
}
Beginner note
Don't calculate offsets. To get the next page, pass the returned nextPageToken into the next jira_search call. The response's isLast tells you when to stop.

Payload trimming

app/jira/trim.py

What 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:

  1. Serialize the result and check byte size.
  2. If it fits, return it.
  3. If not, clip summaries more aggressively.
  4. If still too big, drop trailing issues.
  5. Report how many issues were omitted.
Production note
This is far safer than returning raw Jira JSON, which can break Copilot Studio with 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.

Python
@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:

JSON
{
  "error": "forbidden",
  "status": 403,
  "message": "Jira rejected the delegated token or the user lacks permission."
}

Telemetry & redaction

app/telemetry.py

What 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.

❓ 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.py builds the FastMCP app, registers tools, mounts it at / (so MCP is at /mcp), and adds health endpoints.
  • The token lives in a ContextVar so 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.

βœ… End-of-chapter review

0/5 done