Skip to main content

02 - MCP Primitives: Tools, Resources and Prompts

Overview: The Three Primitives of the MCP Protocol. In the first article of this series, we introduced the Model Context Protocol (MCP) and its architecture.

What do you think about this article?

At a Glance

Overview: The Three Primitives of the MCP Protocol. In the first article of this series, we introduced the Model Context Protocol (MCP) and its architecture.

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 content with type text, image, or resource
  • 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, or sprint://{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:

  1. Name: unique identifier (e.g., create-sprint)
  2. Description: natural language text explaining what the tool does
  3. 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

  1. 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.).
  2. 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.
  3. 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.
  4. 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:

  1. The user selects a Prompt: "Sprint Review" with parameter sprintId: 42
  2. The Prompt generates instructions: the message suggests the AI to use specific tools
  3. The AI calls Tools: invokes get-sprint, calculate-velocity, get-sprint-stories
  4. The application loads Resources: adds config://team-settings as context
  5. 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

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects