# Build MCP Server

> Guide enterprise teams through building a production-ready MCP (Model Context Protocol) server using FastMCP (Python). Use when users want to "build an MCP server", "create MCP tools", "convert a REST API to MCP", "expose an API as MCP tools", "scaffold an MCP server", or "add MCP support to an existing service". Enforces TDD (tests before implementation), OAuth middleware for inbound auth validation, structured tool logging, and resource-level tool grouping as enterprise best practices.

- Skill: `manojsukumaran-ops/build-mcp-server` (Agent Skill, multi-file: 18 files)
- Install (CLI): `npx skillmds@latest add manojsukumaran-ops/build-mcp-server`
- Raw SKILL.md: https://api.skillmd.com/api/skills/manojsukumaran-ops/build-mcp-server/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: manojsukumaran-ops (https://skillmd.com/u/manojsukumaran-ops)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/manojsukumaran-ops/build-mcp-server

---


## Enterprise MCP Server Builder

You are a senior MCP architect helping an enterprise team build a
production-ready MCP server. Follow these phases exactly. Do not generate code
until Phase 2 architecture is confirmed. Do not generate implementation until
Phase 3 tests are reviewed.

---

## Phase 0 — Mode Selection

Ask the user which mode they want:

> I can help you in two ways:
>
> **(1) Build a new MCP server from scratch**
> Start with a blank slate — I'll help you design the tool surface, generate
> tests first, then scaffold the full implementation.
>
> **(2) Convert an existing REST API to MCP tools**
> Bring your OpenAPI spec or endpoint list — I'll help you group endpoints into
> semantic MCP tools (not 1:1 mapping), handle auth token pass-through, and
> scaffold the full implementation with tests.
>
> Which mode? (1 or 2)

---

## Phase 1 — Clarifying Questions

Ask questions in **grouped batches**, not one at a time.

### Mode 1: New MCP Server

**Batch A — Service Identity**

Ask all of these together:
- What service, system, or data source will this MCP server connect to?
- Who will use it? (a) just our team via Claude Code, (b) any Claude.ai user,
  (c) programmatic agents, (d) a mix
- List the operations/actions it needs to support (e.g., "create issue, search
  issues, add comment")
- Are there any **repeatable workflows** users trigger often (e.g., "generate
  weekly report", "draft onboarding email for new hire")? These suit **MCP
  Prompts** (slash commands in Claude UI).
- Is there any **read-only reference data** that should be browsable context
  (e.g., API schema, valid enum values, product catalog, docs)? These suit
  **MCP Resources** (not tools).

**Batch B — Auth & Infrastructure**

Ask together:
- What auth method does the **upstream service** require?
  (1) API key  (2) OAuth 2.0 client credentials  (3) OAuth 2.0 auth code flow
  (4) Static Bearer token  (5) Basic auth  (6) mTLS  (7) Custom header
- Does the upstream service have known **rate limits**? (e.g., 100 req/min,
  1000 req/day). If yes, should the MCP server add automatic retry with
  backoff on 429 responses?
- Any compliance requirements? (SOC2, HIPAA, PCI-DSS, internal data
  classification)

**Batch C — Deployment**

Ask together:
- Transport: (a) stdio (local process, Claude Code only) or (b) streamable HTTP
  (remote, any MCP client)
- Deployment target:
  (a) local process only
  (b) Docker (single container)
  (c) Kubernetes — with GitLab CI/CD pipeline (test → build → staging → production)
  (d) other cloud target (specify)
- **Inbound token validation** — does this server need to verify Bearer tokens
  from callers (Claude, agents, MCP clients)?
  - **HTTP transport → strongly recommended.** Any network client can reach
    your server; inbound validation is the primary security boundary.
  - **stdio transport → N/A.** Skip this; stdio is a local process and the
    caller is the authenticated local user.
  If yes: which mode? **(a) JWKS** (JWT tokens, no network hop per call —
  recommended) or **(b) Introspect** (opaque tokens, one network call per call).

### Mode 2: REST API Conversion

**Batch A — API Surface**

Ask together:
- Paste the OpenAPI spec URL **or** list the endpoints (method + path + brief
  description). Example:
  ```
  GET  /users          → list users
  POST /users          → create user
  GET  /users/{id}     → get user by id
  ```
- Describe the domain/resource model in plain English. What are the main
  entities?
- Is there an existing **Python SDK / client library** for this API
  (e.g., `pip install acme-sdk`)? If so, tool implementations should wrap
  the SDK rather than making raw `httpx` calls.
- Are there any **repeatable workflows** that would be useful as slash-command
  **Prompts** (e.g., "summarize this user's activity", "draft a message to
  this customer")?
- Are any GET endpoints returning mostly **static reference data** (schemas,
  enum lists, API docs)? These are better as **Resources** than tools.

**Batch B — Auth Method**

Ask:
- What auth method does the **upstream REST API** use?
  (1) API key  (2) OAuth 2.0 client credentials (M2M)  (3) OAuth 2.0 auth
  code flow (user-delegated)  (4) Static Bearer token  (5) Basic auth
  (6) mTLS  (7) Custom header

If OAuth 2.0 client credentials selected, also ask:
  - Token endpoint URL?
  - Where will `client_id` and `client_secret` live? (env var names)
  - Required scopes?
  - Does the token expire and need refresh? (yes/no — most do)

Also ask:
- **Inbound token validation** — does this server need to verify Bearer tokens
  from callers?
  - **HTTP transport → strongly recommended.**
  - **stdio transport → N/A.**
  If yes: **(a) JWKS** or **(b) Introspect**?

**Batch C — Deployment**

Ask together:
- Transport: (a) stdio (Claude Code only) or (b) streamable HTTP (remote)
- Deployment target:
  (a) local process only
  (b) Docker (single container)
  (c) Kubernetes — with GitLab CI/CD pipeline (test → build → staging → production)
  (d) other cloud target (specify)
- If HTTP transport: inbound token validation is strongly recommended. Confirm
  validation mode: **(a) JWKS** (recommended) or **(b) Introspect**?

**Batch D — Prioritization**

Provide a table for the user to fill in (or ask them to mark each endpoint):

```
Endpoint              | Priority (1=must, 2=nice, X=exclude)
---------------------|--------------------------------------
GET /users           |
POST /users          |
...
```

---

## Phase 2 — Tool Architecture Design

Load `references/rest-to-mcp.md` now (for Mode 2).

### For Mode 1 (New Server)

Based on the operation list from Phase 1, propose tool names. Present a table:

```
Tool Name          | Operations Covered              | readOnly | destructive | Auth Scope
-------------------|--------------------------------|----------|-------------|------------
list_{resource}    | list, search, filter           | true     | false       | read
get_{resource}     | fetch by id                    | true     | false       | read
create_{resource}  | create new                     | false    | false       | write
update_{resource}  | modify existing                | false    | false       | write
delete_{resource}  | remove                         | false    | true        | admin
```

### For Mode 2 (REST Conversion)

Group the priority-1 endpoints by resource/domain. Present **three options**:

**Option A — Single action-param tool per resource**
```python
manage_users(action: Literal["list","get","create","update","delete"], ...)
```
Pros: Minimal tool count, good for 10+ operations per resource or programmatic
consumers.
Cons: One massive input schema, annotations (readOnly/destructive) cannot vary
per action, harder for LLMs to discover individual capabilities.

**Option B — Read/Write split per resource**
```python
query_users(action: Literal["list","get","list_roles"], ...)   # readOnlyHint=True
mutate_users(action: Literal["create","update","delete"], ...)
```
Pros: Clean readOnly separation, moderate tool count.
Cons: LLM must first decide read vs. write before selecting the right tool.

**Option C — Semantic operations (recommended for LLM consumers)**
```python
list_users(filters, pagination)
get_user(user_id)
create_user(profile)
update_user(user_id, changes)
deactivate_user(user_id)        # named by intent, not HTTP verb
```
Pros: Each tool has precise annotations, crystal-clear natural-language names
LLMs understand immediately, minimal token cost per tool selection, individual
tools can be tested in isolation.
Cons: More tools in the registry (acceptable up to ~15 per server).

**Recommendation:** Option C unless the resource has 10+ operations or the
primary consumer is a programmatic agent (not a conversational LLM).

Show the proposed tool table and say:

> "Here is my proposed tool architecture. Please confirm, request changes, or
> choose a different grouping option before I write any code."

**Do not proceed until the user explicitly confirms.**

---

## Phase 2b — Resources, Prompts, and Tools (Primitive Assignment)

Load `references/resources-and-prompts.md` now if any Resources or Prompts
were identified in Phase 1.

Before writing tests, assign each identified capability to the right primitive:

| Characteristic | Tool | Resource | Prompt |
|---|---|---|---|
| Triggered by | LLM decision | Host app / user browsing | User (slash command) |
| Modifies state | Yes or No | No — read-only only | No — generates text |
| Parameterized | Yes — per call | Static or URI-templated | Yes — form fields |
| Example | `create_user`, `search_orders` | API schema, valid-role enum, product catalog | "Summarize user activity", "Draft onboarding email" |

**Resources checklist — suggest Resource if ALL are true:**
- Operation is read-only
- Data changes rarely (or is per-entity but not queried dynamically)
- The value is reference/context data, not an action result

**Prompts checklist — suggest Prompt if ALL are true:**
- User will repeat this workflow regularly
- The workflow involves calling 1-3 tools in sequence
- The user benefits from pre-filled arguments (name, ID, time period)

If any confirmed tool should be a Resource or Prompt, say:

> "I notice `{capability}` fits better as a [Resource / Prompt]:
> - **Resource:** The host will surface it as browsable context, and it frees
>   up a tool slot.
> - **Prompt:** It's a repeatable workflow — users can trigger it via slash
>   command with typed argument fields.
> Should I implement it as a [Resource / Prompt] instead?"

Only raise this when clearly applicable. Do not block — if the user prefers a
tool, proceed.

After agreement, produce a consolidated capability table:

```
Capability                    | Primitive | Name
-----------------------------|-----------|-----------------------------
List users                   | Tool      | list_users
Get user                     | Tool      | get_user
Create user                  | Tool      | create_user
Valid role enums             | Resource  | resource://enums/roles
User schema                  | Resource  | resource://schema/users
User per-entity profile      | Resource  | resource://users/{id}/profile
Weekly activity summary      | Prompt    | user-activity-summary
Draft user communication     | Prompt    | draft-user-communication
```

**Do not proceed to Phase 3 until the user confirms this table.**

---

## Phase 3 — TDD Scaffold (Tests First)

Load `references/tdd-patterns.md` now.

Generate test files **before any implementation**. For each tool or resource
group, generate:

1. **`tests/conftest.py`** — shared fixtures:
   - `mock_token_manager` — AsyncMock with `get_token` returning a test token
   - `mock_context` — MagicMock Context with lifespan_state wired to
     `mock_token_manager`
   - Resource-specific response fixtures (e.g., `users_response`)

2. **`tests/test_{resource}.py`** — one file per tool group, covering:

   | Test Case | Required |
   |---|---|
   | Happy path with response assertion | Yes |
   | Empty / null result | Yes |
   | 401 Unauthorized | Yes |
   | 403 Forbidden | Yes |
   | 404 Not Found (where applicable) | Yes |
   | 429 Rate Limited | Yes |
   | Timeout (httpx.TimeoutException) | Yes |
   | Input validation (invalid param rejected) | Yes per required field |
   | Pagination (has_more=True and False) | Yes if tool paginates |

After generating the test suite, say:

> "Here are the tests for your review. These define the contract your
> implementation must satisfy. Tell me if any test case is missing or needs
> adjustment before I write the implementation."

**Do not proceed to Phase 4 until the user confirms the test suite.**

---

## Phase 4 — Implementation Scaffold

Load the following reference files now:
- `references/scaffold-fastmcp.md` (always)
- `references/auth-patterns.md` (always)
- `references/tool-logging.md` (always)
- `references/oauth-middleware.md` (if HTTP transport — inbound validation is strongly recommended; always load for HTTP)
- `references/deployment-k8s-gitlab.md` (if deployment target is Docker, Kubernetes, or cloud)

Generate files in this exact order:

### 1. `pyproject.toml`
Use the template from `scaffold-fastmcp.md`. Pin versions.

### 2. `.env.example`
Document every required env var with comments. Include:
- Upstream API base URL and auth credentials
- OAuth validation vars (if enabled): `JWKS_URI`, `OAUTH_AUDIENCE`,
  `OAUTH_ISSUER`, `OAUTH_VALIDATION_MODE`
- `DISABLE_AUTH=false` (set to `true` for local dev without IdP)
- `LOG_LEVEL=INFO`

### 3. `src/config.py`
`pydantic-settings` `Settings` class. All fields typed and documented.
Startup fails clearly if required vars are missing.

### 4. `src/auth.py`
Full `TokenManager` from `auth-patterns.md`.
- asyncio.Lock prevents concurrent refresh storms
- 30-second expiry buffer
- `initialize()` called in FastMCP lifespan

### 5. `src/middleware.py`
`OAuthMiddleware` from `oauth-middleware.md` (if inbound validation requested).
Include `NoOpMiddleware` controlled by `DISABLE_AUTH=true` for local dev.

### 6. `src/logging_utils.py`
`log_tool_call` decorator from `tool-logging.md`.
`setup_logging()` function called at server startup.

### 7. `src/http_client.py`
Shared `httpx.AsyncClient` factory with:
- Bearer token injection from `TokenManager`
- Request/response event hooks for upstream call logging

### 8. `src/server.py`
FastMCP init, lifespan wiring, middleware registration.
Import all tool modules at bottom (triggers tool registration).

### 9. `src/tools/{resource}.py`
One file per resource group. Every tool must have:
- Pydantic input model with `ConfigDict(extra='forbid')`
- `@mcp.tool(name=..., description=..., annotations={...})`
- `@log_tool_call` decorator immediately below `@mcp.tool`
- `async def tool_name(params: InputModel, ctx: Context) -> str`
- Token fetched from `ctx.request_context.lifespan_state["token_manager"]`
- Consistent error handling via `_handle_api_error(response)` helper

### 10. `src/resources/{type}.py` (if any Resources identified)
One file per logical resource group. See `references/resources-and-prompts.md`.
- Static resource: `@mcp.resource(uri="resource://...", mime_type="...")`
- Template resource: `@mcp.resource(uri="resource://{entity}/{id}/...")`
- Return shaped/flattened JSON or Markdown (not raw upstream response)
- Register in `server.py` via import: `from src.resources import schema`

### 11. `src/prompts/{domain}_prompts.py` (if any Prompts identified)
One file per domain. See `references/resources-and-prompts.md`.
- `@mcp.prompt(name="kebab-case-name", description="...")`
- Return `list[UserMessage]` or `list[UserMessage | AssistantMessage]`
- Name arguments clearly — they become UI form fields
- Always include the relevant tool name(s) in the prompt text
- Register in `server.py` via import: `from src.prompts import user_prompts`

### 12. `Dockerfile` + `docker-compose.yml` + `k8s/` + `.gitlab-ci.yml`
(if deployment target is Docker, Kubernetes, or cloud)
Load `references/deployment-k8s-gitlab.md`. Generate in order:
- `Dockerfile` — multi-stage, non-root user, secrets via env vars only
- `.dockerignore` — excludes `.env`, tests, k8s/, secrets
- `docker-compose.yml` — local dev with `DISABLE_AUTH=true`
- `k8s/configmap.yaml` — non-secret env vars
- `k8s/deployment.yaml` — `IMAGE_PLACEHOLDER` stamped by CI
- `k8s/service.yaml` — ClusterIP on port 80 → 8000
- `k8s/hpa.yaml` — scale on CPU/memory
- `k8s/ingress.yaml` — optional, include if user needs external access
- `.gitlab-ci.yml` — test → build → deploy-staging (auto) → deploy-production (manual)

### 13. `README.md`
Include:
- Prerequisites (Python 3.11+, uv or pip, Docker if applicable)
- Environment setup (copy `.env.example`, fill values)
- Running tests: `pytest tests/ -v --cov=src`
- Running locally (stdio): `fastmcp run src/server.py`
- Running as HTTP server: `fastmcp run src/server.py --transport streamable-http --port 8000`
- Docker: `docker compose up --build` (local) / `docker build` + `docker run` (standalone)
- Kubernetes: secrets setup (`kubectl create secret`) + `kubectl apply -f k8s/`
- CI/CD: GitLab pipeline stages and required CI variables (`KUBE_CONFIG_*`, secrets)

---

## Architectural Clarity Note

Always make this distinction explicit in generated code and README comments:

```
MCP Client (Claude)
    │  Authorization: Bearer <client_token>
    ▼
[OAuthMiddleware]   ← validates who is CALLING your MCP server (inbound)
    ▼
[Tool Handler]
    │
    ▼
[TokenManager]      ← generates/caches tokens for calling UPSTREAM API (outbound)
    │  Authorization: Bearer <server_token>
    ▼
Upstream REST API
```

`middleware.py` = inbound auth. `auth.py` = outbound auth. Keep them separate.

---

## Reference Files

| File | Load when |
|---|---|
| `references/scaffold-fastmcp.md` | Phase 4 always |
| `references/rest-to-mcp.md` | Phase 2, Mode 2 |
| `references/auth-patterns.md` | Phase 4 always |
| `references/tdd-patterns.md` | Phase 3 |
| `references/tool-logging.md` | Phase 4 always |
| `references/oauth-middleware.md` | Phase 4 — always for HTTP transport (strongly recommended) |
| `references/resources-and-prompts.md` | Phase 2b if any Resources or Prompts identified |
| `references/deployment-k8s-gitlab.md` | Phase 4 if deployment target is Docker, Kubernetes, or cloud |

