The landscape of Artificial Intelligence is shifting rapidly from isolated chat interfaces to integrated, context-aware systems. At the heart of this evolution lies the Model Context Protocol (MCP), an open standard designed to standardize how applications provide context to Large Language Models (LLMs). While consuming MCP clients is becoming increasingly common, the true power of the ecosystem lies in building robust, high-performance MCP servers. This guide provides a technical deep-dive into constructing these servers, moving beyond basic tutorials to address architectural considerations, tool definitions, and resource management.
Understanding the Server Architecture
Before writing a single line of code, it is crucial to understand the role of the MCP server. Unlike traditional REST or GraphQL APIs, an MCP server does not just serve data; it serves capabilities. It exposes two primary primitives to the client: Tools and Resources.
Tools are functions that the LLM can execute, such as running a database query or sending an email. Resources are static or dynamic data sources, like reading a configuration file or fetching a live stock price. A well-architected MCP server strictly separates these concerns while maintaining a unified transport layer, typically JSON-RPC over stdio or HTTP.
Defining Tools with Precision
The accuracy of an LLM's tool usage depends heavily on how you define your schemas. In TypeScript, using the official @modelcontextprotocol/sdk, you define tools using the Zod schema validator to ensure type safety and clear descriptions.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "my-awesome-server",
version: "1.0.0"
});
// Define a tool to search a documentation index
server.tool(
"search_docs",
"Search the internal documentation for technical queries",
{
query: z.string().describe("The search query string"),
limit: z.number().default(5).describe("Max results to return")
},
async ({ query, limit }) => {
// Implementation logic here
const results = await myDocSearchEngine.search(query, limit);
return {
content: results.map(r => ({
type: "text",
text: JSON.stringify(r)
}))
};
}
);
Note the emphasis on the description and parameter description fields. These are not just for documentation; they are the primary signals the LLM uses to determine when and how to invoke your tool. Ambiguity here leads to hallucinated arguments or refusal to use the tool.
Handling Resources and Prompts
Beyond tools, MCP servers can expose resources via URIs. This allows clients to request data dynamically. For example, you might expose a resource at docs://config/latest that returns the current application configuration.
Additionally, MCP supports Prompts, which are predefined sequences of instructions or templates that users can invoke. This is distinct from tools because prompts generate text for the user or LLM to read, rather than executing code. Implementing prompts requires registering a prompt handler that returns a structured message list.
server.prompt(
"summarize_code",
"Generate a concise summary of the provided code snippet",
{ filePath: z.string() },
async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please summarize this code:\n\n${content}`
}
}
]
};
}
);
Best Practices for Production
- Error Handling: Always wrap your tool logic in try-catch blocks. Return structured error messages that the LLM can understand, rather than raw stack traces.
- Streaming: For long-running operations, implement streaming responses. This improves the user experience by providing real-time feedback.
- Security: Never expose internal network addresses or sensitive credentials as resources. Validate all inputs rigorously using Zod to prevent injection attacks.
Conclusion
Building MCP servers is about more than just connecting an API to an LLM; it is about structuring data and logic in a way that maximizes the utility of the model. By focusing on clear tool definitions, robust error handling, and secure resource management, you can create integrations that are not only functional but also reliable and scalable. As the MCP ecosystem matures, expect to see more sophisticated servers that leverage these primitives to power the next generation of AI-driven applications.