본문으로 건너뛰기
L13.4

MCP Server Design

Goal

Build a small MCP 2026-07-28 server around one clear application capability, validate its inputs, and keep permissions and real-world effects under application control.

One narrow capability is enough​

Suppose a support application needs to check an order status. The underlying system may have hundreds of database operations, but the client does not need “run any SQL” or “call any internal function.”

A deliberately small tool is easier to understand:

get_order_status(order_id="4172")
→ {order_id: "4172", status: "delivered"}

An MCP server can expose that operation in a standard protocol shape. Compatible clients can then discover and call it. The server description explains the tool. Its input shape says that order_id is required. The implementation maps the request to the application's real order service.

The protocol does not decide who is allowed to see order 4172. Your application still has to check identity, permissions, and business rules before returning data. In the same way, a tool called refund_order should not gain unlimited refund authority merely because it is exposed through MCP.

So begin with the smallest useful capability. Give it a clear name and input shape, validate the request, and keep sensitive effects behind the same application policy you would require without MCP.

Make stateless requests self-contained​

The pinned MCP release uses stateless core requests. In simple terms, each request must carry the protocol information needed to understand it. Your application may still call stateful internal services, but the MCP boundary should not rely on a hidden per-client protocol session.

Discovery must describe capabilities accurately. If a tool can change data, do not describe it like a read-only query. Clients make safer choices when names and effects match reality.

Validate before business logic​

Validate before doing business work. Check the protocol version, operation metadata, tool name, and argument shape. Then send the normalized request through the application's normal policy and service layer. The MCP handler must not bypass authorization.

request
→ validate version + method + arguments
→ apply local policy
→ call narrow handler
→ return typed result or typed error

Keep error types distinct. An unknown tool, bad arguments, unsupported version, policy denial, and internal service failure need different responses. A client can then decide whether to fix input, stop, or use an allowed retry.

Record evidence at the boundary. Useful fields include trace ID, tool name, protocol version, validation result, policy result, latency, and final outcome. Do not log secrets or extra personal data just because a structured request contains them.

Test failures and upgrades​

Test more than the happy path. Include fixtures for unknown tools, missing arguments, version mismatch, conflicting metadata, and denied callers. These tests exercise the boundary without a public server.

An official SDK removes protocol boilerplate, but it does not choose your authority rules. Your application still owns capability scope, policy, side-effect safety, and telemetry.

Make upgrades explicit too. Record the protocol release the server expects. Reject incompatible requests with a clear compatibility error instead of partly interpreting an unfamiliar message.

Predict

An MCP server has an existing internal admin function that can modify any account. What is the best exposure strategy?

Run the deterministic server-boundary preflight​

Run:

python3 labs/notebooks/level-13/l13-04-mcp-server.py

The required Lab is a deterministic server-boundary preflight, not a wire-level MCP server.

  1. Run it unchanged. The main request should return order status and print business function calls: 1.
  2. Find the main request = ... "name": "get_order_status" ... line. Before editing, predict both the response category and the business-call count if the requested capability is not declared.
  3. Change only "name": "get_order_status" to "name": "admin_anything" in that main request, then rerun.
  4. Confirm the response contains unknown_tool and business function calls: 0. The mock business function must not execute for a rejected capability. Restore the starter afterward.

Loading lab…

Write the core logic yourself​

Open:

labs/notebooks/level-13/l13-04-mcp-server-exercise.py

Implement the deterministic MCP server preflight: protocol-version rejection, undeclared-tool rejection, and required argument-type validation. Then continue to the official-SDK extension below.

Run:

python3 labs/notebooks/level-13/l13-04-mcp-server-exercise.py

The starter intentionally fails at its TODO boundary. A completed implementation ends with a PASS: marker. Compare with the solved deterministic Lab only after attempting the implementation yourself.

Build the real MCP server with the official SDK​

Once the boundary rules make sense, install the optional environment and run the official-SDK extension:

pip install -r labs/real-model/requirements.txt
python labs/real-model/l13_mcp_client.py

This time there is a real MCPServer. The client negotiates the protocol and calls list_tools(). It discovers get_order_status, invokes it through call_tool(), and reads the structured result. The in-process transport keeps setup small while still exercising the SDK's real MCP lifecycle.

You can also open the same server with the SDK development tooling:

uv run --with "mcp[cli]>=2,<3" mcp dev labs/real-model/l13_mcp_server.py

Compare the generated tool schema with the deterministic preflight. The SDK removes protocol boilerplate; it does not decide whether a capability should exist or whether the current principal is authorized to use it.

Quick Check

1. What is a strong MCP server capability design?
2. Where should business authorization run?
3. Why keep error categories distinct?

0 of 3 questions answered.

Explain it back​

Describe a one-tool MCP server for reading an order. Include capability description, argument validation, application authorization, trace fields, and two negative tests.

Key Takeaways

  • Expose deliberate, narrow server capabilities.
  • Validate protocol and arguments before business execution.
  • Do not bypass existing application authorization.
  • Keep compatibility, input, policy, and service errors distinguishable.
  • SDKs handle mechanics, not your trust and policy design.

Next Lesson

Next, L13.5 — Connect an MCP Client examines discovery, local filtering, request construction, and result validation.

References

Lesson actions

Completion is stored locally on this device.

View progress