Chapter 7 · Build
Build Your Own MCP Server
The repeatable 15-step method distilled from the reference implementation. Use it to design tools, choose an auth model, add trimming and tests, and ship a connector — for Jira or any other system.
🎯 What you'll be able to do
- Define the user and system boundary before writing code
- Choose a safe authentication model and design narrow tools
- Scaffold a Python MCP project with the right dependencies
- Plan trimming, tests, the connector, container, and deployment
Step 1 — Define the user and system boundary
Answer these before writing any code:
| Question | Example answer for Jira |
|---|---|
| Who is the user? | A Copilot Studio user signed into Atlassian. |
| What system is called? | Jira Cloud. |
| Does the call run as user or app? | User. |
| Where are tokens stored? | Power Platform connector, not the MCP server. |
| What endpoint does the agent call? | /mcp. |
| What gateway fronts the server? | Azure API Management. |
Step 2 — Pick the authentication model
For enterprise user data, prefer delegated identity.
✓ Prefer
- Delegated per-user OAuth tokens
- Tokens held by the connector
- Request-scoped, never stored
- Refresh handled by the platform
✗ Avoid
- Service accounts
- Shared API keys
- Hardcoded tokens
- Refresh tokens stored in your server
Step 3 — Design tools
✓ Good tools are…
- Small and clear
- Permission-safe and predictable
- Documented by their docstring
- Compact in response size
✗ Bad tools are…
- Vague or multi-purpose
- Raw query passthroughs without guardrails
- Unlimited search operations
- Returning entire upstream payloads
| Better | Worse |
|---|---|
jira_get_issue(issue_key) | jira_api(method, path, body) |
jira_search(jql, max_results, next_page_token) | run_any_jql_without_limits(query) |
jira_add_comment(issue_key, body) | modify_issue_arbitrarily(payload) |
Steps 4–6 — Scaffold, dependencies, configuration
Minimum Python project shape:
my-mcp-server
|-- app
| |-- __init__.py
| |-- server.py
| |-- config.py
| |-- middleware.py
| |-- context.py
| |-- errors.py
| |-- tools/__init__.py
|-- tests
|-- pyproject.toml
|-- Dockerfile
|-- README.mdThe reference dependency set:
mcp>=1.2.0
starlette>=0.37.0
uvicorn[standard]>=0.30.0
httpx>=0.27.0
pydantic>=2.7.0
pydantic-settings>=2.3.0.env.example for placeholders, never real secrets. Categories: host/port, allowed origins, gateway secret, upstream base URL, payload limits, and telemetry.Steps 7–11 — Middleware, client, trimming, tools, tests
- Middleware: origin checks, bearer extraction, gateway integrity, request-scoped token binding, cleanup in
finally. - Upstream client: build URLs from settings, attach the delegated token, set timeouts, retry only transient failures, convert errors safely, return trimmed models.
- Trimming: limit fields, cap page size, use cursor paging, enforce a byte budget, note omitted results.
- Tools: typed arguments, clear docstring, use the client, return a dict or model, covered by tests.
- Tests: start with unit tests (token extraction, origin allow-list, missing token, trimming, paging), then integration tests (initialize, tools/list, a real call, auth failure, gateway failure, upstream 401/403/429/5xx).
Steps 12–15 — Connector, container, deploy, operate
For Copilot Studio MCP, expose a single /mcp POST operation and include the agentic-protocol extension:
"x-ms-agentic-protocol": "mcp-streamable-1.0"| Connector field | Value |
|---|---|
| host | APIM host, e.g. example-apim.azure-api.net |
| basePath | /jira-mcp |
| paths | /mcp |
| OAuth provider | Generic OAuth 2 |
| Authorization URL | https://auth.atlassian.com/authorize?audience=api.atlassian.com&prompt=consent |
| Token URL | https://auth.atlassian.com/oauth/token |
Then containerize (multi-stage slim image, non-root user, health check, python -m app.server), deploy behind APIM, and operate with logs, health and readiness probes, autoscale, and secret rotation. Details are in Deployment and Security.
🛠️ Mini-project: Port the scaffold to a new tool
Without writing production code yet, design a new read tool for a system you know (e.g. ServiceNow incidents or GitHub issues). Write down: the tool name and typed arguments, the smallest useful response model, the page-size cap, and one unit test you would add. Compare your design against the “good tools” checklist above.
❓ Concept check
Your upstream system does support per-user OAuth, but a colleague wants to use one service account “to keep it simple.” Which step of this method addresses that, and what is the correct call?
📌 Chapter summary
- Start from the boundary (who is the user, what system, user or app identity) and let it drive every later decision.
- Prefer delegated identity; design small, clear, capped tools; return trimmed models.
- Keep the FastMCP structure, request-scoped token, middleware, APIM, Key Vault, trimming, tests, connector, and Dockerfile patterns.