Overview: The Three MCP Protocol Primitives
In the first article of this series, we introduced the Model Context Protocol (MCP) and its Host/Client/Server architecture. Now it is time to analyze in detail the three primitives that an MCP server can expose: Tools, Resources, and Prompts.
Each primitive has a distinct role in the interaction between AI and the external world. Understanding the differences, use cases, and internal structure of each one is fundamental to designing effective and well-structured MCP servers. In this article, we will cover the theory, validation schemas with Zod, JSON-RPC payloads, and TypeScript handlers, analyzing concrete examples from the Tech-MCP project.
What You Will Learn in This Article
- The difference between Tools, Resources, and Prompts: role, control, and use cases
- How to define a Tool with Zod validation and an asynchronous handler
- How to expose Resources with URI templates and read-only data
- How to create parameterized and reusable Prompts
- The JSON-RPC payload format for each primitive
- The lifecycle of an MCP session from discovery to shutdown
- Best practices for tool descriptions and naming conventions
The Three Primitives Compared
Before diving into each primitive in detail, it is useful to have an overview of their fundamental differences. Each primitive addresses a specific need within the MCP ecosystem:
MCP Primitives Summary
| Primitive | Role | Who Controls | Side Effects | Example |
|---|---|---|---|---|
| Tools | Functions invocable by AI | AI decides autonomously | Yes (can modify state) | create-sprint, analyze-diff |
| Resources | Read-only accessible data | Host application | No (read-only) | sprint://{id}, file:///config |
| Prompts | Reusable predefined templates | User selects | No (generate messages) | sprint-review, code-analysis |
This distinction matters: Tools are the operational core of the protocol, Resources provide context and data, and Prompts guide the AI through structured tasks. Let us examine each primitive in detail.
1. Tools: Functions the AI Can Invoke
Tools represent the core of MCP interaction. They are functions that the AI can call autonomously during a conversation to perform concrete actions in the real world. Unlike a simple API call, an MCP tool explicitly declares its parameters with a typed schema, allowing the AI to understand when and how to invoke it.
Fundamental Tool Characteristics
- AI decides when to call them: based on conversation context, the language model autonomously chooses which tool to invoke and with what parameters
- Parameters typed with Zod: each tool declares its inputs with a Zod schema, ensuring automatic runtime validation
- Structured results: the result is always an array of
contentwith typetext,image, orresource - Can have side effects: create files, write to databases, call external APIs, send notifications
- Composable: the AI can combine multiple tools in sequence to complete complex tasks
Anatomy of a Tool in TypeScript
Tool registration follows a precise structure with four elements: unique name, description for the AI, Zod parameter schema, and asynchronous handler. Here is a complete example from the Tech-MCP project:
import { z } from 'zod';
server.tool(
'create-sprint', // Unique tool name
'Create a new sprint with a name, date range, and goals. ' +
'Returns the created sprint object with id, status, and dates. ' +
'Use this when the user wants to start planning a new iteration.',
{ // Parameter schema (Zod)
name: z.string().describe('Sprint name, e.g. Sprint-15'),
startDate: z.string().describe('Start date in ISO format'),
endDate: z.string().describe('End date in ISO format'),
goals: z.array(z.string()).describe('List of sprint goals'),
},
async ({ name, startDate, endDate, goals }) => { // Handler
const sprint = store.createSprint({
name,
startDate,
endDate,
goals
});
return {
content: [{
type: 'text',
text: JSON.stringify(sprint, null, 2)
}]
};
}
);
Why Zod for Validation?
Zod is a TypeScript-first validation library that offers complete type-safety at compile-time and validation at runtime. The MCP SDK automatically converts Zod schemas into JSON Schema, the format the AI uses to understand tool parameters. This means that by declaring a Zod schema you get: automatic input validation, documentation generation for the AI, and TypeScript type inference, all in a single definition.
Tool Response Format
Every tool handler must return an object with a content array. Each element in
the array has a type indicating the content format:
// Text response (most common)
return {
content: [{
type: 'text',
text: 'Sprint created successfully: Sprint-15 (ID: 42)'
}]
};
// Image response
return {
content: [{
type: 'image',
data: base64EncodedImage,
mimeType: 'image/png'
}]
};
// Error response
return {
content: [{
type: 'text',
text: 'Error: sprint not found'
}],
isError: true
};
Tool Categories in the Tech-MCP Project
In the Tech-MCP project, the 85+ tools are organized into functional categories based on the type of operation they perform:
Tool Classification by Operation Type
| Category | Example Tool | Effects | Description |
|---|---|---|---|
| Creation | create-sprint, save-snippet |
Write to database | Create new persistent entities |
| Read | get-sprint, search-snippets |
Read-only | Retrieve existing data |
| Analysis | analyze-diff, find-bottlenecks |
No side effects | Process inputs and generate insights |
| Generation | generate-unit-tests, generate-compose |
Produce output | Generate code, configurations, templates |
| Monitoring | list-pipelines, get-budget-status |
Read external state | Check the status of external systems |
2. Resources: Read-Only Accessible Data
Resources are the second MCP primitive. They represent data that the host application can read and provide as context to the AI. Unlike Tools, Resources are not called directly by the AI: it is the host application (Claude Desktop, Cursor, etc.) that decides when and which resources to load into the conversation context.
Fundamental Resource Characteristics
- Identified by URI: each resource has a unique identifier, such as
file:///path/to/file,db://schema/table, orsprint://{id} - Application requests them: unlike tools where AI decides, for resources the host application controls access
- Read-only: resources do not modify server state, they serve exclusively to provide data
- Support URI Templates: for parametric resources, templates like
sprint://{id}can be defined - Explicit MIME type: each resource declares its content type (
application/json,text/plain, etc.)
Defining a Resource with URI Template
Here is how to define a parametric resource that exposes sprint details identified by its ID:
server.resource(
'sprint://{id}', // URI template
'Get sprint details by ID', // Description
async (uri) => { // Handler
const id = extractIdFromUri(uri);
const sprint = store.getSprint(id);
if (!sprint) {
throw new Error(`Sprint ${id} not found`);
}
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(sprint, null, 2)
}]
};
}
);
Static vs Dynamic Resources
MCP resources fall into two categories: static and dynamic. Static resources have a fixed URI and always return data from the same endpoint. Dynamic resources use URI templates to accept parameters.
// Static resource: fixed URI, global data
server.resource(
'config://app-settings',
'Application configuration settings',
async (uri) => {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(config.getAll())
}]
};
}
);
// Dynamic resource: URI template with parameter
server.resource(
'user://{userId}/profile',
'User profile data',
async (uri) => {
const userId = extractParam(uri, 'userId');
const profile = await db.getProfile(userId);
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(profile)
}]
};
}
);
JSON-RPC Payload for Resources
When the host application requests a resource, the MCP protocol uses two JSON-RPC methods:
resources/list to discover available resources and resources/read
to read their content.
// Request: list of available resources
{ "method": "resources/list" }
// Server response
{
"resources": [
{
"uri": "config://app-settings",
"name": "App Settings",
"mimeType": "application/json"
}
],
"resourceTemplates": [
{
"uriTemplate": "sprint://{id}",
"name": "Sprint Details",
"mimeType": "application/json"
}
]
}
// Request: read a specific resource
{ "method": "resources/read",
"params": { "uri": "sprint://42" } }
// Server response
{
"contents": [{
"uri": "sprint://42",
"mimeType": "application/json",
"text": "{ \"id\": 42, \"name\": \"Sprint-15\" }"
}]
}
Note on the Tech-MCP Project
In the Tech-MCP project, data interaction happens primarily through Tools. Resources are planned for future development, for example to expose project configurations, database schemas, or API documentation as read-only context for the AI.
3. Prompts: Reusable Predefined Templates
Prompts are the third MCP primitive. They represent predefined templates that guide the AI in executing complex and structured tasks. Unlike tools (where AI decides) and resources (where the application decides), prompts are selected by the user to initiate specific workflows.
Fundamental Prompt Characteristics
- Guide the AI: provide structured instructions that direct the language model toward a specific objective
- Accept arguments: can be parameterized with user input
- Combinable with tools: a prompt can suggest a sequence of tools for the AI to call in order
- Reusable: once defined, they can be used across different conversations with different parameters
- User-selected: the user chooses which prompt to activate, not the AI
Defining a Prompt with Arguments
An MCP prompt is defined with a name, a description, a list of arguments, and a function that generates the messages to send to the AI:
server.prompt(
'sprint-review', // Unique name
'Generate a comprehensive sprint review report', // Description
[ // Arguments
{
name: 'sprintId',
description: 'ID of the sprint to review',
required: true
},
{
name: 'format',
description: 'Output format: summary, detailed, or metrics-only',
required: false
}
],
async ({ sprintId, format }) => ({ // Handler
messages: [{
role: 'user',
content: {
type: 'text',
text: `Analyze sprint ${sprintId} with the following steps:
1. Use the get-sprint tool to retrieve sprint data
2. Use calculate-velocity to compute team velocity
3. Use get-sprint-stories to list completed stories
4. Generate a report in ${format || 'detailed'} format with:
- Goals achieved vs planned
- Velocity and trend compared to previous sprints
- Completed and incomplete stories
- Recommendations for the next sprint`
}
}]
}))
);
JSON-RPC Payload for Prompts
Like the other primitives, prompts use dedicated JSON-RPC methods for discovery and invocation:
// Discovery: list of available prompts
{ "method": "prompts/list" }
// Server response
{
"prompts": [
{
"name": "sprint-review",
"description": "Generate a comprehensive sprint review report",
"arguments": [
{
"name": "sprintId",
"description": "ID of the sprint to review",
"required": true
},
{
"name": "format",
"description": "Output format: summary, detailed, or metrics-only",
"required": false
}
]
}
]
}
// Invoking a prompt
{ "method": "prompts/get",
"params": {
"name": "sprint-review",
"arguments": { "sprintId": "42", "format": "detailed" }
}
}
// Response: messages generated by the prompt
{
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": "Analyze sprint 42 with the following steps..."
}
}]
}
Prompts that Orchestrate Tools
The true power of prompts emerges when they are used to orchestrate complex sequences of tools. A well-designed prompt can guide the AI through a multi-step workflow:
server.prompt(
'full-code-review',
'Perform a comprehensive code review workflow',
[
{ name: 'filePath', description: 'Path to the file to review', required: true },
{ name: 'language', description: 'Programming language', required: true }
],
async ({ filePath, language }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Perform a comprehensive code review of ${filePath} (${language}):
STEP 1 - Static analysis:
Use analyze-diff to identify issues in the code
STEP 2 - Test generation:
Use generate-unit-tests to create tests for the functions found
STEP 3 - Style verification:
Check that the code follows project conventions
STEP 4 - Final report:
Generate a report with: issues found, suggested tests,
proposed improvements, severity of each issue`
}
}]
}))
);
As with Resources, the Tech-MCP project currently uses Tools as its primary primitive. Prompts are planned for future development, with the goal of creating predefined workflows for recurring scenarios such as sprint reviews, new developer onboarding, and team performance analysis.
How AI Chooses Tools: The Role of Descriptions
When an MCP server registers with a client, it exposes the list of available tools via the
tools/list method. For each tool, the AI receives three key pieces of information:
- Name: unique identifier (e.g.,
create-sprint) - Description: natural language text explaining what the tool does
- Parameter schema: structure of input parameters with types and descriptions
The description is the most critical element: a well-written description allows the AI to understand when to use the tool, what to expect as a result, and in which scenarios it is appropriate to invoke it.
Best Practices for Tool Descriptions
EXCELLENT (explains what it does, what it returns, when to use it):
"Create a new sprint with a name, date range, and goals.
Returns the created sprint object with id, name, dates, status.
Use this when the user wants to start planning a new iteration."
GOOD (explains what it does and inputs):
"Create a new sprint with a name, date range, and goals"
VAGUE (AI might not understand when to use it):
"Sprint creation tool"
Checklist for Effective Descriptions
| Element | Question | Example |
|---|---|---|
| Action | What does the tool do? | "Create a new sprint..." |
| Input | What parameters are needed? | "...with a name, date range, and goals" |
| Output | What does it return? | "Returns the created sprint object with id..." |
| Context | When to use it? | "Use when the user wants to plan a new iteration" |
The MCP Session Lifecycle
Every MCP interaction follows a structured lifecycle in four phases. Understanding this flow is essential for designing servers that behave correctly in every scenario:
[1] INITIALIZATION
Client --> Server: initialize (protocol version, capabilities)
Server --> Client: capabilities (tool list, resource templates, prompts)
[2] DISCOVERY
Client --> Server: tools/list
Server --> Client: [{ name, description, inputSchema }, ...]
Client --> Server: resources/list
Server --> Client: [{ uri, name, mimeType }, ...]
Client --> Server: prompts/list
Server --> Client: [{ name, description, arguments }, ...]
[3] USAGE (repeated N times during the session)
Client --> Server: tools/call { name, arguments }
Server --> Client: { content: [...] }
Client --> Server: resources/read { uri }
Server --> Client: { contents: [...] }
Client --> Server: prompts/get { name, arguments }
Server --> Client: { messages: [...] }
[4] SHUTDOWN
Client --> Server: close
Phase Details
- Initialization: the client sends the protocol version and its capabilities. The server responds by declaring which primitives it supports (tools, resources, prompts) and its own capabilities (notifications, logging, etc.).
- Discovery: the client queries the server to discover all available primitives. This phase is fundamental because it provides the AI with the information needed to decide which tools to use.
- Usage: the operational phase, repeated N times during the session. The client can invoke tools, read resources, and activate prompts in any order and combination.
- Shutdown: the client signals the end of the session. The server releases resources and closes connections.
Parameter Validation with Zod: Advanced Examples
Zod offers a wide range of validators that allow you to define precise schemas for tool parameters. Here are some advanced patterns used in the Tech-MCP project:
import { z } from 'zod';
// Schema with enum and optional values
const createTaskSchema = {
title: z.string().min(1).max(200)
.describe('Task title'),
priority: z.enum(['low', 'medium', 'high', 'critical'])
.describe('Task priority level'),
assignee: z.string().optional()
.describe('Username of the assignee'),
labels: z.array(z.string()).default([])
.describe('List of labels to apply'),
estimate: z.number().positive().optional()
.describe('Estimated hours to complete'),
};
// Schema with nested objects
const deployConfigSchema = {
environment: z.enum(['staging', 'production']),
version: z.string().regex(/^\d+\.\d+\.\d+$/),
options: z.object({
rollback: z.boolean().default(true),
healthCheck: z.boolean().default(true),
notifySlack: z.boolean().default(false),
}).optional(),
};
// Schema with union types
const searchSchema = {
query: z.string().min(1),
scope: z.union([
z.literal('code'),
z.literal('docs'),
z.literal('issues'),
z.literal('all'),
]).default('all'),
limit: z.number().int().min(1).max(100).default(20),
};
Primitive Interplay: A Complete Example
The three MCP primitives do not operate in isolation. In a real scenario, a workflow can combine all three primitives synergistically:
- The user selects a Prompt: "Sprint Review" with parameter
sprintId: 42 - The Prompt generates instructions: the message suggests the AI to use specific tools
- The AI calls Tools: invokes
get-sprint,calculate-velocity,get-sprint-stories - The application loads Resources: adds
config://team-settingsas context - The AI generates the report: combines all collected data into a structured report
This composability is one of the key strengths of the MCP protocol: each primitive contributes its specific role to complex and articulated workflows.
Conclusions
The three MCP primitives -- Tools, Resources, and Prompts -- constitute the fundamental vocabulary of the protocol. Each primitive has a distinct role: Tools execute actions, Resources provide data, and Prompts guide the AI through structured tasks.
Validation with Zod ensures type-safety and automatic documentation, while the MCP session lifecycle ensures orderly and predictable interaction between client and server. Composability between primitives allows building complex workflows from simple, well-defined building blocks.
In the next article, we will dive into monorepo architecture and project patterns used in Tech-MCP: how to organize 31 MCP servers in a single repository, manage shared dependencies with Turborepo and pnpm, and structure code for maximum reusability and maintainability.
The complete code for all examples is available in the Tech-MCP GitHub repository.
Commenti
Caricamento commenti...
Sign in to comment