Skip to content

9.5.4 MCP Server Development

  • Understand the minimum responsibility boundary of an MCP Server
  • Learn how to define tool descriptions, parameter structures, and invocation entry points
  • Understand why the focus of server development is “exposing capabilities,” not “hard-coding business logic”
  • Read and understand a minimal runnable Mock MCP Server

It is not “just another regular backend”

Section titled “It is not “just another regular backend””

A regular backend usually exposes business APIs directly. An MCP Server is more like:

Organizing existing capabilities into a set of tools that can be discovered and called by the client.

So its core concerns are usually:

  • What tools are available
  • How each tool is described
  • How parameters are validated
  • How results are returned in a consistent way

An MCP Server is a bit like a tool library manager with a front desk:

  • The client asks, “What tools do you have here?”
  • The server lists its capability inventory
  • The client then says, “Which one should I use?”
  • The server executes according to the contract and returns the result

This is very different from “just writing all the business functions loosely scattered around.”


At minimum, it should have:

  • A name
  • A description
  • Parameter specifications
  • Actual execution logic
search_docs_tool = {
"name": "search_docs",
"description": "Search course documents and return relevant content",
"parameters": {
"query": {
"type": "string",
"description": "The keyword to search for"
}
},
"required": ["query"]
}
print(search_docs_tool)

Expected output:

Terminal window
{'name': 'search_docs', 'description': 'Search course documents and return relevant content', 'parameters': {'query': {'type': 'string', 'description': 'The keyword to search for'}}, 'required': ['query']}

You can think of this structure as:

The public-facing instruction manual for the tool.


Why can’t tool descriptions be too casual?

Section titled “Why can’t tool descriptions be too casual?”
bad_tool = {
"name": "search",
"description": "Do search",
"parameters": {"q": {"type": "string"}}
}
print(bad_tool)

Expected output:

Terminal window
{'name': 'search', 'description': 'Do search', 'parameters': {'q': {'type': 'string'}}}

The problems are:

  • The name is too vague
  • The description is too empty
  • The meaning of the parameter is unclear
good_tool = {
"name": "search_course_docs",
"description": "Search course FAQ, policies, and learning path documents",
"parameters": {
"query": {
"type": "string",
"description": "The topic the user wants to query, such as refund policy or certificate"
}
},
"required": ["query"]
}
print(good_tool)

Expected output:

Terminal window
{'name': 'search_course_docs', 'description': 'Search course FAQ, policies, and learning path documents', 'parameters': {'query': {'type': 'string', 'description': 'The topic the user wants to query, such as refund policy or certificate'}}, 'required': ['query']}

What is better here:

  • The tool boundary is clearer
  • The parameter semantics are clearer
  • The client is more likely to use it correctly

The two minimum capabilities of a Server: list tools + call tools

Section titled “The two minimum capabilities of a Server: list tools + call tools”

A minimal usable MCP Server usually needs to be able to:

  1. List available tools
  2. Accept a tool invocation
class MockMCPServer:
def __init__(self):
self.tool_specs = [
{
"name": "search_docs",
"description": "Search course documents",
"parameters": {
"query": {"type": "string"}
}
}
]
def list_tools(self):
return self.tool_specs
server = MockMCPServer()
print(server.list_tools())

Expected output:

Terminal window
[{'name': 'search_docs', 'description': 'Search course documents', 'parameters': {'query': {'type': 'string'}}}]
class MockMCPServer:
def __init__(self):
self.kb = {
"refund": "You can request a refund within 7 days after purchase if your learning progress is below 20%.",
"certificate": "You can receive a certificate after completing all projects and passing the tests."
}
self.tool_specs = [
{
"name": "search_docs",
"description": "Search course documents",
"parameters": {
"query": {"type": "string"}
}
}
]
def list_tools(self):
return self.tool_specs
def call_tool(self, name, arguments):
if name != "search_docs":
return {"error": "unknown_tool"}
query = arguments.get("query", "")
for key, value in self.kb.items():
if key in query:
return {"result": value}
return {"result": "No relevant documents found"}
server = MockMCPServer()
print(server.call_tool("search_docs", {"query": "What is the refund policy?"}))

Expected output:

Terminal window
{'result': 'You can request a refund within 7 days after purchase if your learning progress is below 20%.'}

This is already a very clear minimal server skeleton.


Why is parameter validation one of the server’s responsibilities?

Section titled “Why is parameter validation one of the server’s responsibilities?”

Because the client or the model may pass incorrect parameters

Section titled “Because the client or the model may pass incorrect parameters”

For example:

bad_call = {"query_text": "refund policy"}

If the server executes this directly, it may crash or behave strangely.

def validate_search_docs(arguments):
if "query" not in arguments:
return False, "missing_query"
if not isinstance(arguments["query"], str):
return False, "query_must_be_string"
return True, "ok"
print(validate_search_docs({"query": "refund policy"}))
print(validate_search_docs({"query_text": "refund policy"}))

Expected output:

Terminal window
(True, 'ok')
(False, 'missing_query')

Because the server is the gatekeeper of the capability boundary. If the server does not validate, the entire tool system becomes hard to keep stable.

MCP Server Tool Contract Diagram


class BetterMCPServer:
def __init__(self):
self.kb = {
"refund": "You can request a refund within 7 days after purchase if your learning progress is below 20%.",
"certificate": "You can receive a certificate after completing all projects and passing the tests."
}
def list_tools(self):
return [
{
"name": "search_docs",
"description": "Search course documents",
"parameters": {
"query": {"type": "string"}
}
}
]
def validate(self, name, arguments):
if name != "search_docs":
return False, "unknown_tool"
if "query" not in arguments:
return False, "missing_query"
if not isinstance(arguments["query"], str):
return False, "query_must_be_string"
return True, "ok"
def call_tool(self, name, arguments):
ok, msg = self.validate(name, arguments)
if not ok:
return {"error": msg}
query = arguments["query"]
for key, value in self.kb.items():
if key in query:
return {"result": value}
return {"result": "No relevant documents found"}
server = BetterMCPServer()
print(server.list_tools())
print(server.call_tool("search_docs", {"query": "How do I get a certificate?"}))
print(server.call_tool("search_docs", {"wrong": "How do I get a certificate?"}))

Expected output:

Terminal window
[{'name': 'search_docs', 'description': 'Search course documents', 'parameters': {'query': {'type': 'string'}}}]
{'result': 'You can receive a certificate after completing all projects and passing the tests.'}
{'error': 'missing_query'}

MCP Server validation result map

What is better about this version than the previous one?

Section titled “What is better about this version than the previous one?”

It already has:

  • Tool listing
  • Parameter validation
  • A unified invocation entry point
  • Unified error returns

This is already very close to the core responsibilities of a server in real-world engineering.


Keep this page’s proof of learning as a small evidence card:

Capability
resource, prompt, or tool exposed by server
Contract
schema, transport, permissions, and error shape
Call Trace
discovery, invocation, response, and failure handling
Failure Check
incompatible schema, missing auth, unsafe tool, or server error
Integration Action
validate server contract before adding autonomy

The most common pitfalls in MCP Server development

Section titled “The most common pitfalls in MCP Server development”

This leads to:

  • Unclear tool descriptions
  • Harder extension
  • Harder debugging

Tool granularity that is too coarse or too fine

Section titled “Tool granularity that is too coarse or too fine”
  • Too coarse: one tool does everything
  • Too fine: client invocation complexity explodes

Sometimes returning text, sometimes dicts, sometimes raising exceptions directly makes future integration very difficult.


How do you know whether an MCP Server design is good enough?

Section titled “How do you know whether an MCP Server design is good enough?”

You can start by asking four questions:

  1. Can the client clearly know what tools are available?
  2. Are the parameter requirements explicit?
  3. Are error returns consistent?
  4. Will the structure become messy when adding new tools?

If you can answer all four questions confidently, the server design is usually already pretty good.


The most important thing in this section is not “writing a class,” but understanding:

The essence of an MCP Server is to expose a set of executable capabilities in a way that is clear to discover, validate, and call.

The clearer the server is, the easier it is for the client side to expand, and the easier it is to grow the whole tool ecosystem.


  1. Add a new get_weather(city) tool to BetterMCPServer.
  2. Add parameter validation logic for this new tool.
  3. Think about it: what problems do too coarse and too fine tool granularity each cause?
  4. Explain in your own words: why is the core of MCP Server development not just “executing tools,” but “exposing clear boundaries”?
Reference implementation and walkthrough
  1. Add get_weather(city) as a registered tool with a description and schema, then return a small structured result such as {city, condition, source}. Use placeholder data unless the page already connects to a real API.
  2. Validate that city exists, is a non-empty string, and is not an unexpectedly large payload. Invalid input should return a clear error instead of silently guessing.
  3. Too coarse a tool hides important choices and makes errors hard to localize. Too fine a tool forces the Agent to plan too many tiny calls and increases latency, routing mistakes, and context clutter.
  4. MCP Server development is about exposing a boundary: what the tool does, what input it accepts, what output and errors it returns, and what permissions it needs. Execution is only one part of that contract.