AI & Agentic Engineering

DAY 15: MCP Roots vs Resources vs Tools: 7 Powerful Data Access Patterns Every AI Engineer Must Master

MCP Roots vs Resources vs Tools explains the practical difference between scope, information, and executable capabilities in production MCP servers, including security, testing, code examples, and architecture patterns.

46 min read
DAY 15: MCP Roots vs Resources vs Tools: 7 Powerful Data Access Patterns Every AI Engineer Must Master
Advertisement
What You Will Learn
Understanding MCP Roots vs Resources vs Tools
What Are MCP Roots?
Why MCP Roots Matter
Root URIs
⚡ Quick Answer
MCP Roots define the permitted filesystem locations or scope for an AI application. MCP Resources expose data for the AI to read, while MCP Tools provide specific operations the AI can execute. Distinguishing these concepts is crucial for QA engineers and SDETs to ensure secure, maintainable, and predictable AI system behavior.

MCP gives AI applications several ways to interact with information and capabilities, but MCP Roots vs Resources vs Tools is one of the most important distinctions to understand before building serious MCP systems.

At first glance, Roots, Resources, and Tools can appear similar because all three can participate in an AI agent workflow.

They are not interchangeable.

A useful mental model is:

MCP Roots
    ↓
Define where the client allows access

MCP Resources
    ↓
Expose data for the client or model to read

MCP Tools
    ↓
Provide actions the model can execute

The difference becomes especially important when an MCP server works with files, databases, APIs, source code, documents, or other external systems.

If these concepts are mixed together, an MCP implementation can become unnecessarily powerful, difficult to secure, and harder for an AI model to reason about.

Understanding MCP Roots vs Resources vs Tools

The simplest distinction is based on purpose.

MCP PrimitivePrimary PurposeTypical BehaviorExample
RootsDefine allowed filesystem locationsProvides scope/context/workspace/project
ResourcesExpose readable informationRead/access datafile:///project/config.json
ToolsPerform actionsExecute operationscreate_issue()

Think of an MCP-powered coding assistant.

The user might allow the application to work inside:

/workspace/my-project

That location can act as a Root.

The MCP server might expose:

file:///workspace/my-project/README.md

as a Resource.

The server could provide:

run_tests()

as a Tool.

The relationship becomes:

Root
 └── /workspace/my-project
       │
       ├── Resource
       │     └── README.md
       │
       ├── Resource
       │     └── config.json
       │
       └── Tool
             └── run_tests()

The Root establishes the permitted location.

The Resource represents information.

The Tool performs an operation.

What Are MCP Roots?

MCP Roots provide a way for an MCP client to communicate filesystem locations that are relevant to the current context.

For example:

file:///workspace/my-project

could represent the project directory that an AI coding assistant is working with.

The important idea is that a Root is primarily about scope, not about performing an operation.

A Root does not mean:

"Read this file."

It means something closer to:

"This location is relevant and permitted within this client context."

That distinction matters when designing secure MCP applications.

Why MCP Roots Matter

Imagine an AI coding assistant working on:

/home/user/projects/payment-service

Without clear boundaries, a poorly designed server could potentially attempt to access unrelated locations such as:

/home/user/.ssh
/home/user/.aws
/home/user/secrets

A controlled Root provides an important contextual boundary.

Conceptually:

Allowed
/home/user/projects/payment-service

versus:

Outside intended scope
/home/user/.ssh
/home/user/.aws

Roots therefore become especially useful when an MCP application works with local project files.

Root URIs

A Root is represented using a URI.

For example:

file:///workspace/project

Another example could be:

file:///Users/developer/projects/demo

The exact path depends on the environment.

The important part is that the Root identifies a location rather than an executable operation.

Conceptually:

{
  "uri": "file:///workspace/project",
  "name": "My Project"
}

The client communicates the Root information to the server as part of the MCP interaction.

Roots Are Not File Reads

This is one of the most important concepts in MCP Roots vs Resources vs Tools.

A Root does not automatically mean that the model has received the contents of every file beneath that location.

For example:

Root:
file:///workspace/project

does not automatically mean:

README.md contents
config.json contents
database.json contents
.env contents

have all been loaded into the model context.

Instead, the Root establishes contextual scope.

The application still needs an appropriate mechanism to access specific data.

That is where Resources can become useful.

What Are MCP Resources?

MCP Resources are designed to expose information that an MCP client can retrieve.

A Resource can represent information such as:

Files
Documents
Database records
Configuration
Logs
Documentation
Application state
Generated data

Resources are generally about data access, whereas Tools are about actions.

For example:

Resource:
file:///workspace/project/README.md

could represent a document that the AI application can read.

Another Resource might represent:

postgres://database/users

depending on the server’s design and supported URI scheme.

The important concept is that the Resource identifies data that can be made available to the client or model.

Static and Dynamic Resources

Resources can represent different types of information.

A relatively static Resource might be:

file:///workspace/project/README.md

A dynamic Resource might represent changing application information.

For example:

orders://customer/12345

could conceptually identify customer order information.

The server can determine how that URI maps to actual data.

This creates an abstraction between the AI application and the underlying data source.

The model does not necessarily need to understand:

PostgreSQL
Redis
Filesystem
REST API
Object storage

as separate implementation mechanisms.

It can work with MCP’s Resource abstraction.

Resource Example in Python

A simplified MCP server can expose a Resource using an MCP Python SDK.

Conceptually, the code can look like:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ProjectServer")


@mcp.resource("project://readme")
def readme() -> str:
    return """
# Demo Project

This project contains an MCP server.
"""


if __name__ == "__main__":
    mcp.run()

The exact SDK APIs can evolve between MCP SDK versions, so production implementations should always be aligned with the version being used.

The important architectural idea is:

Resource URI
      ↓
MCP Server
      ↓
Data

The Resource describes what data can be accessed.

What Are MCP Tools?

Tools are different.

A Tool represents an operation that an MCP client or AI model can invoke.

Examples include:

create_issue()
search_repository()
run_tests()
send_email()
query_database()
create_ticket()
deploy_application()

A Tool answers:

What can the system do?

A Resource answers:

What information can the system provide?

A Root answers:

What location or scope is relevant?

This distinction makes MCP Roots vs Resources vs Tools much easier to understand.

A Simple Tool Example

Consider a calculator tool:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


if __name__ == "__main__":
    mcp.run()

The Tool exposes an executable capability:

add(10, 20)

The result is:

30

This is fundamentally different from a Resource.

A Resource provides data.

A Tool performs an operation.

The Three Concepts Through a Real Example

Imagine building an MCP server for a software repository.

The client provides:

Root:
file:///workspace/payment-service

The server exposes Resources:

project://README
project://architecture
project://package-config

The server provides Tools:

search_code()
run_tests()
create_branch()

The architecture becomes:

                 MCP Client
                     │
          ┌──────────┼──────────┐
          │          │          │
        Roots     Resources    Tools
          │          │          │
          ↓          ↓          ↓
       Scope        Data       Actions

This separation is extremely useful.

MCP Roots vs Resources vs Tools: The Core Comparison

QuestionRootsResourcesTools
What is it?Scope/contextDataCapability/action
Main purposeDefine relevant locationsExpose informationExecute operations
Typically read?Metadata/contextYesNo, usually executes
Performs action?NoNoYes
Represents data?IndirectlyYesNot primarily
Represents permission boundary?Can establish scopeNot primarilyNot primarily
ExampleProject directoryREADMERun tests
AI interactionContextInformationAction

The most important rule is:

Roots = Where
Resources = What data
Tools = What action

This simple model is useful when designing MCP servers.

Why the Difference Matters for AI Agents

AI agents need to reason about both information and actions.

Suppose an agent receives:

"Why is the payment service failing its tests?"

A good MCP architecture might allow the agent to:

1. Understand the project Root
2. Read relevant Resources
3. Inspect source information
4. Invoke a Tool to run tests
5. Analyze the result

The workflow could become:

Root
 ↓
Project Scope
 ↓
Resources
 ↓
Source Code / Documentation
 ↓
Tool
 ↓
Run Tests
 ↓
Tool Result
 ↓
AI Reasoning

This is much clearer than exposing every operation as a Tool.

Why Everything Should Not Be a Tool

A common beginner mistake is turning every piece of information into an executable Tool.

For example:

get_readme()
get_config()
get_documentation()
get_schema()
get_logs()

could all technically be implemented as Tools.

But if the operation is fundamentally a request to retrieve information, a Resource may represent the concept more naturally.

Instead:

Resources
 ├── README
 ├── Configuration
 ├── Documentation
 └── Schema

Then Tools can focus on actions:

Tools
 ├── run_tests
 ├── update_config
 └── create_issue

This gives the AI application a clearer capability model.

Why Everything Should Not Be a Resource

The opposite mistake is also possible.

Suppose you expose:

orders://customer/123

as a Resource.

That may be appropriate for reading order information.

But if the AI needs to:

cancel_order()

that operation should not simply be treated as passive data access.

Cancellation changes state.

A Tool is more appropriate:

cancel_order(order_id)

So:

Read order
    ↓
Resource

Cancel order
    ↓
Tool

The distinction is particularly important for destructive operations.

Read vs Action

A useful teaching model is:

READ
 ↓
Resource

DO
 ↓
Tool

For example:

Read customer profile
        ↓
Resource

Update customer profile
        ↓
Tool

Another:

Read deployment status
        ↓
Resource

Start deployment
        ↓
Tool

This mental model is not a substitute for MCP’s formal protocol definitions, but it is extremely useful when designing server interfaces.

Understanding Safety Boundaries

The distinction also matters for security.

Compare:

Resource:
read application logs

with:

Tool:
delete application logs

The second capability has substantially greater potential impact.

Similarly:

Resource:
read database records

versus:

Tool:
delete database records

The Tool introduces an action that changes state.

This means Tool design deserves careful attention to:

Authorization
Input validation
Audit logging
Confirmation
Error handling
Least privilege

A Practical Design Rule

When designing an MCP capability, ask three questions:

Is this defining scope?

If yes, consider:

Root

Is this exposing information?

If yes, consider:

Resource

Is this performing an operation?

If yes, consider:

Tool

This gives you a practical decision tree:

                    Capability
                        │
             ┌──────────┼──────────┐
             │          │          │
           Scope       Data      Action
             │          │          │
           Root      Resource     Tool

Learning Exercise: Classify MCP Capabilities

Consider these seven capabilities:

1. Project directory
2. README file
3. Database schema
4. Search repository
5. Run tests
6. Delete temporary files
7. Application logs

Classify them:

CapabilityLikely MCP Concept
Project directoryRoot
README fileResource
Database schemaResource
Search repositoryTool
Run testsTool
Delete temporary filesTool
Application logsResource

The classification is based on the primary purpose of each capability.

There can be implementation-specific nuances, but this is the correct starting point for architectural thinking.

Designing a Repository MCP Server

Imagine a repository server.

A clean design could be:

MCP Repository Server
│
├── Roots
│   └── Project Workspace
│
├── Resources
│   ├── README
│   ├── Architecture
│   ├── Configuration
│   └── Documentation
│
└── Tools
    ├── Search Code
    ├── Run Tests
    ├── Create Branch
    └── Create Issue

The model can then reason about the system using three distinct categories:

Where can I work?
        ↓
Roots

What information can I inspect?
        ↓
Resources

What actions can I perform?
        ↓
Tools

This separation becomes increasingly valuable as the MCP server grows.

The Architecture Behind the Abstraction

A typical workflow might look like:

AI Application
      │
      │ MCP
      ↓
MCP Client
      │
      ├──────── Roots
      │
      └──────── MCP Server
                    │
          ┌─────────┼─────────┐
          ↓         ↓         ↓
       Resources   Tools    Server Logic
          │         │
          ↓         ↓
        Data      Actions

The MCP server becomes a controlled interface between the AI system and external capabilities.

That is one of the reasons the distinction between MCP Roots vs Resources vs Tools is foundational for MCP development.

A Common Architectural Mistake

Avoid designing a server like this:

Everything → Tool

For example:

read_file()
read_config()
read_document()
read_schema()
run_test()
delete_file()
update_config()

This makes the Tool namespace larger than necessary.

A more intentional design might be:

Resources
 ├── File
 ├── Config
 ├── Document
 └── Schema

Tools
 ├── Run Test
 ├── Delete File
 └── Update Config

The result is easier for humans and AI models to understand.

Designing for Least Privilege

A strong MCP server should expose only the capabilities that the application actually needs.

Suppose an AI coding assistant only needs to:

Read source files
Run tests

There may be little justification for exposing:

delete_files()
execute_shell_command()
modify_database()
deploy_application()

The architecture should follow:

Required capability
       ↓
Minimal exposure
       ↓
Controlled execution

This principle becomes increasingly important as MCP servers connect AI systems to real production environments.

Code-Level Separation

A simple Python project might organize its MCP server like this:

mcp_server/
│
├── server.py
├── resources.py
├── tools.py
└── config.py

For example:

# server.py

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ProjectServer")

Resources can be grouped separately:

# resources.py

def get_project_document():
    return "# Project Documentation"

Tools can be kept separate:

# tools.py

def run_tests():
    return "Tests executed"

In a real implementation, these components would be registered with the MCP server according to the SDK and protocol version being used.

The architectural principle remains:

Resources → data access
Tools     → operations
Roots     → client-defined scope

A Useful Mental Model for MCP Engineers

Imagine an office.

The Root is the building or workspace you are allowed to operate within.

The Resource is a document, file, report, or piece of information inside that workspace.

The Tool is an action you can perform, such as submitting a form or running a process.

Root
= Where am I allowed to work?

Resource
= What information can I access?

Tool
= What can I do?

Once this model becomes intuitive, many MCP architecture decisions become easier.

What Happens When These Concepts Are Mixed?

Suppose a server exposes:

read_project()
write_project()
delete_project()
execute_project()

Everything becomes a generic operation.

The AI model has to infer which operations are:

Read
Write
Delete
Execute

A more structured MCP design makes these boundaries explicit.

Resources
    ↓
Readable information

Tools
    ↓
Actions that can change or operate on state

This improves discoverability and can make the system easier to secure and test.

Building Better MCP Server Interfaces

A well-designed MCP server should make its capabilities understandable.

For example:

Resources:
- project://readme
- project://architecture
- project://configuration

Tools:
- search_code
- run_tests
- create_issue

From this interface, an AI agent can quickly infer:

README → information
Architecture → information
Configuration → information

Search → action
Run tests → action
Create issue → action

This clarity is one of the most important design principles behind MCP Roots vs Resources vs Tools.

The Seven Design Patterns to Remember

The concepts introduced in this section can be reduced to seven practical patterns:

1. Root defines scope
2. Resource exposes information
3. Tool performs an operation
4. Resources should not be used for arbitrary actions
5. Tools should not expose unnecessary read operations
6. Destructive operations require stronger controls
7. Least privilege should guide capability design

These patterns will become increasingly important when building production MCP servers with databases, filesystems, APIs, and AI agents.

Designing MCP Roots, Resources, and Tools Correctly

The difference between MCP Roots vs Resources vs Tools becomes much more important when moving from simple examples to real MCP servers.

A production MCP server should not expose capabilities simply because they are technically possible. Each capability should have a clear purpose, predictable behavior, and an appropriate security boundary.

A useful design principle is:

Scope → Root
Data  → Resource
Action → Tool

This separation helps both developers and AI agents understand the server’s capabilities.

Designing MCP Roots for Real Projects

Roots are particularly useful when an MCP client works with local project directories.

Consider a software project:

/workspace/
└── payment-service/
    ├── src/
    ├── tests/
    ├── docs/
    ├── config/
    └── README.md

The client could identify:

file:///workspace/payment-service

as the relevant Root.

The important point is that the Root establishes context. It does not mean that every file inside the directory should automatically be exposed to the model.

A secure implementation should still determine which resources can be accessed and which operations can be performed.

Root Scope Is Not the Same as Permission

One of the most important concepts when learning MCP Roots vs Resources vs Tools is that a Root should not be treated as a magical security mechanism.

Suppose the client provides:

file:///workspace/project

That does not automatically make every filesystem operation safe.

A poorly designed server might still expose a Tool such as:

@mcp.tool()
def read_any_file(path: str):
    ...

An attacker or malicious prompt could attempt:

../../.env

or:

../../.ssh/id_rsa

Therefore, server-side validation remains essential.

The architecture should be:

Client Root
     ↓
Server validates scope
     ↓
Requested operation
     ↓
Authorization
     ↓
Access

Never assume that client-provided context alone is sufficient protection.

Validating File Paths

A filesystem-oriented MCP server should validate requested paths against allowed Roots.

A simplified Python example:

from pathlib import Path


def safe_path(root: str, requested: str) -> Path:
    root_path = Path(root).resolve()
    requested_path = (root_path / requested).resolve()

    if root_path not in requested_path.parents:
        raise PermissionError("Path is outside the allowed root")

    return requested_path

Then:

path = safe_path(
    "/workspace/payment-service",
    "src/app.py"
)

print(path)

A malicious request such as:

../../.env

should be rejected.

This demonstrates an important distinction:

Root
= Context provided by the client

Path validation
= Security responsibility of the server

Designing MCP Resources

Resources should represent information in a way that is meaningful to the client and model.

Poor resource design:

resource://thing1
resource://thing2
resource://thing3

Good resource design communicates meaning:

project://readme
project://architecture
project://config
project://tests

The URI becomes part of the interface.

A developer looking at:

project://architecture

can immediately understand what it represents.

An AI agent can also reason about it more effectively.

Resource Naming Strategy

Resource identifiers should be:

  • predictable
  • descriptive
  • stable
  • meaningful
  • consistent

For example:

project://docs/getting-started
project://docs/architecture
project://config/application
project://logs/application

is easier to understand than:

resource://1
resource://2
resource://3
resource://4

Good naming becomes increasingly important as the number of Resources grows.

Resource MIME Types

Resources may represent different kinds of content.

For example:

text/plain
text/markdown
application/json
text/csv
application/xml

A server can use appropriate metadata to help the client understand what it is receiving.

Conceptually:

{
  "uri": "project://config",
  "mimeType": "application/json"
}

This distinction becomes useful when an MCP application consumes structured data.

For example:

{
  "environment": "staging",
  "database": "orders",
  "debug": true
}

is naturally understood as JSON rather than arbitrary text.

Static Resources vs Dynamic Resources

Not every Resource needs to represent a physical file.

A Resource can conceptually represent:

Static
→ README.md

Dynamic
→ Current deployment status

Generated
→ Database schema

Computed
→ Application metrics

For example:

deployment://production/status

could represent the current state of a deployment.

The underlying information might come from:

Kubernetes
Cloud provider
CI/CD system
Database
Monitoring API

The AI application interacts with the Resource abstraction rather than needing to understand every underlying implementation.

Resource Templates

When Resources contain dynamic identifiers, Resource Templates become useful.

Imagine customer data:

customer://123
customer://456
customer://789

Rather than manually registering every possible customer, a template can conceptually describe:

customer://{customer_id}

This allows the server to represent dynamic resources.

For example:

customer://123
customer://456
customer://789

all follow the same pattern.

The architecture becomes:

Resource Template
       ↓
Dynamic URI
       ↓
Server resolves identifier
       ↓
Data source
       ↓
Resource content

This is particularly useful for:

Users
Orders
Tickets
Products
Documents
Database records

Designing MCP Tools

Tools should represent meaningful actions.

For example:

search_code
run_tests
create_issue
update_ticket
send_notification
deploy_application

Each Tool should ideally have a narrow responsibility.

Avoid a generic Tool like:

execute_anything(command)

when specialized operations can provide better control.

Instead of:

@mcp.tool()
def execute(command: str):
    ...

prefer:

@mcp.tool()
def run_tests(test_path: str) -> str:
    ...

The second interface is easier to understand and control.

Tool Input Schemas

Tools need well-defined inputs.

For example:

@mcp.tool()
def create_issue(
    title: str,
    description: str,
    priority: str
) -> str:
    ...

The inputs communicate the intended operation.

A stronger design can also constrain values:

priority:
- low
- medium
- high
- critical

Instead of allowing arbitrary input:

priority = "whatever"

Schema design is therefore an important part of Tool safety.

Tool Validation

Never assume that AI-generated arguments are correct.

Suppose a Tool accepts:

@mcp.tool()
def delete_user(user_id: int) -> str:
    ...

The server should still validate:

Does the user exist?
Is the caller authorized?
Is deletion permitted?
Is the ID valid?
Should deletion require confirmation?
Should the operation be logged?

A safer conceptual workflow is:

Tool Request
     ↓
Schema Validation
     ↓
Authentication
     ↓
Authorization
     ↓
Business Validation
     ↓
Execution
     ↓
Audit Logging

The Tool should never jump directly from model-generated arguments to a destructive operation.

Read Tools vs Action Tools

A Tool does not necessarily have to modify data.

For example:

search_repository()

can be an action that performs a search.

Similarly:

calculate_tax()

performs computation without necessarily changing persistent state.

Therefore, the simple rule:

Tool = writes data

is incorrect.

A better definition is:

Tool = executable capability

Some Tools read or calculate.

Others modify state.

The risk depends on what the Tool can actually do.

MCP Roots vs Resources vs Tools: Capability Risk

Consider these examples:

CapabilityCategoryTypical Risk
Project directory scopeRootMedium
READMEResourceLow
Application logsResourceMedium
Search repositoryToolLow–Medium
Run testsToolMedium
Update configurationToolHigh
Delete database recordsToolCritical
Deploy productionToolCritical

This demonstrates why Tools require particularly careful design.

Not all Tools are equally dangerous.

Designing Least-Privilege Tools

Suppose an AI coding agent needs to run tests.

A risky Tool might expose:

execute_shell(command)

The model could potentially execute arbitrary commands.

A narrower Tool could expose:

run_tests(test_file)

The difference is significant.

Instead of:

AI
 ↓
Arbitrary Shell

the architecture becomes:

AI
 ↓
run_tests()
 ↓
Approved Test Runner

The second approach reduces the capability surface.

Tool Granularity

There is a balance between overly broad and overly narrow Tools.

Consider:

Tool A:
execute_everything()

Tool B:
run_unit_tests()

Tool C:
run_integration_tests()

Tool D:
run_lint()

Tool A provides maximum flexibility but also maximum risk.

Tools B–D provide more explicit capabilities.

However, creating hundreds of tiny Tools can also make the interface difficult to navigate.

A practical approach is:

Clear business capability
+
Controlled input
+
Predictable output
+
Minimal privilege

Comparing Broad and Narrow Tools

DesignFlexibilitySecurityDiscoverabilityRecommendation
execute(command)Very HighLowMediumAvoid when possible
run_tests()MediumHighHighGood
run_test_suite(name)HighHighHighExcellent
manage_system()Very HighLowLowAvoid
create_issue()FocusedHighHighGood

The best Tool is usually not the most powerful Tool.

It is the Tool that provides exactly the capability the application needs.

Designing Tool Outputs

Tool outputs should be useful to the AI model.

Poor output:

Done.

Better:

{
  "status": "passed",
  "tests": 42,
  "failures": 0,
  "duration_ms": 1834
}

The structured result provides more information for reasoning.

For example:

Tests: 42
Passed: 42
Failed: 0
Duration: 1834 ms

The model can now explain the result or decide what to do next.

Resources and Tools Working Together

Real MCP applications often need both Resources and Tools.

Imagine an issue-management server.

Resources:

issue://123
issue://456
issue://789

Tools:

create_issue()
update_issue()
close_issue()
assign_issue()

The workflow becomes:

Read issue
   ↓
Resource

Understand issue
   ↓
AI reasoning

Modify issue
   ↓
Tool

This creates a clean separation between information and action.

Roots, Resources, and Tools Working Together

Now add a filesystem Root:

Root:
file:///workspace/project

The complete system becomes:

                    MCP Client
                        │
                        │
                 ┌──────┴──────┐
                 │             │
               Root          Server
                 │             │
          Project Scope   ┌────┴─────┐
                          │          │
                     Resources     Tools
                          │          │
                       Data       Actions

For a coding assistant:

Root
→ /workspace/project

Resources
→ README
→ source documentation
→ configuration

Tools
→ search code
→ run tests
→ create issue

This is a strong architectural foundation for an MCP development environment.

Example: Building a Project Assistant

Suppose you want an AI assistant that helps developers understand a repository.

The requirements are:

Read project documentation
Read architecture information
Search source code
Run tests
Create issues

A clean design could be:

ROOT
file:///workspace/project

RESOURCES
project://readme
project://architecture
project://configuration

TOOLS
search_code(query)
run_tests(scope)
create_issue(title, description)

The AI agent now has a clear capability map.

Where?
→ Root

What information?
→ Resources

What actions?
→ Tools

Example: Database MCP Server

Now consider a database-oriented MCP server.

A Resource might represent:

database://schema

Another:

database://table/users

Tools might include:

query_database(sql)
create_user(...)
update_user(...)

However, exposing arbitrary SQL execution can introduce significant security risk.

A safer design may use narrowly defined operations:

find_user(email)
create_user(name, email)
update_user(user_id, fields)

This creates a smaller attack surface.

The difference is:

Generic database Tool
        ↓
High flexibility
High risk

Domain-specific Tools
        ↓
Controlled flexibility
Lower risk

Learning Exercise: Improve an MCP Interface

Start with this hypothetical server:

Tools:
execute_command()
read_file()
write_file()
delete_file()
database_query()

The interface is powerful but broad.

Redesign it.

A more controlled design could be:

Root:
file:///workspace/project

Resources:
project://readme
project://config
project://architecture

Tools:
search_code(query)
run_tests(scope)
update_config(key, value)

If deletion is genuinely required:

delete_project_file(path)

should have strict validation and authorization rather than being replaced with unrestricted command execution.

Testing MCP Capability Boundaries

A strong MCP test suite should verify more than whether Tools work.

It should also verify boundaries.

For a filesystem server:

Valid:
src/app.py

Invalid:
../../.env

For a Tool:

Valid:
run_tests("unit")

Invalid:
run_tests("../../")

For authorization:

Authorized user
    ↓
Tool executes

Unauthorized user
    ↓
Tool rejected

This is where MCP testing begins to overlap with security testing.

A Practical Capability Matrix

Before implementing an MCP server, document every capability.

CapabilityTypeRead/ExecuteState ChangeRisk
Project directoryRootContextNoMedium
READMEResourceReadNoLow
LogsResourceReadNoMedium
Search codeToolExecuteNoLow
Run testsToolExecuteUsually NoMedium
Update configToolExecuteYesHigh
Delete dataToolExecuteYesCritical
DeployToolExecuteYesCritical

This table can become part of the server’s design documentation.

Teaching Pattern: Scope, Data, Action

When explaining MCP Roots vs Resources vs Tools to a new MCP engineer, use three questions:

Question 1: Where?

Where is the AI allowed or expected to work?

Think:

Root

Question 2: What?

What information does the AI need to inspect?

Think:

Resource

Question 3: What can it do?

What operation does the AI need to perform?

Think:

Tool

The complete model is:

WHERE → Root
WHAT  → Resource
DO    → Tool

This simple framework makes the design of more complex MCP servers considerably easier.

Production Design Checklist

Before exposing a new MCP capability, ask:

1. Is this scope, data, or action?
2. Is Root the correct abstraction?
3. Should this be a Resource?
4. Does this actually need to be a Tool?
5. Can the Tool be narrower?
6. Are inputs validated?
7. Is authorization required?
8. Can the operation modify state?
9. Can the operation access sensitive data?
10. Is the output useful to the AI?
11. Can the capability be abused?
12. Can the operation be audited?

The more powerful the capability, the more carefully these questions should be answered.

The Key Architectural Principle

The strongest MCP designs do not simply expose everything an underlying system can do.

They create an intentional interface:

Underlying System
       ↓
MCP Server
       ↓
Controlled Capabilities
       ↓
MCP Client
       ↓
AI Model

MCP Roots vs Resources vs Tools is therefore not just a terminology lesson.

It is an architectural decision about how an AI system receives context, accesses information, and performs actions.

A well-designed MCP server makes those boundaries explicit from the beginning.

Building and Testing MCP Roots, Resources, and Tools Together

Understanding MCP Roots vs Resources vs Tools becomes much more practical when these concepts are combined into one working MCP server.

A real AI agent rarely uses only one MCP primitive.

A coding assistant may need:

Root
  ↓
Identify project scope

Resource
  ↓
Read project information

Tool
  ↓
Perform an operation

Tool Result
  ↓
Continue reasoning

This creates a capability chain where each MCP primitive has a distinct responsibility.

Building a Filesystem-Aware MCP Server

Consider an MCP server designed for a software project.

The client provides a project Root:

file:///workspace/my-app

The server exposes Resources:

project://readme
project://architecture
project://config

And Tools:

search_code()
run_tests()

The architecture becomes:

MCP Client
    │
    ├── Root
    │     └── file:///workspace/my-app
    │
    └── MCP Server
          │
          ├── Resources
          │     ├── project://readme
          │     ├── project://architecture
          │     └── project://config
          │
          └── Tools
                ├── search_code()
                └── run_tests()

The important point is that the Root does not replace Resources, and Resources do not replace Tools.

They complement each other.

Creating a Project Resource

A Resource can expose project documentation.

A simplified Python implementation using the MCP Python SDK can look like:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Project Assistant")


@mcp.resource("project://readme")
def project_readme() -> str:
    return """
# Payment Service

A Python API responsible for payment processing.

Main components:
- API
- Database
- Payment Gateway
- Background Workers
"""

The Resource provides information.

It does not execute an operation.

The AI can consume the information and use it as context for subsequent reasoning.

Creating a Search Tool

Now suppose the AI needs to search source code.

That is an operation, so a Tool is appropriate.

from pathlib import Path


@mcp.tool()
def search_code(query: str) -> list[str]:
    """Search Python source files for a text pattern."""

    results = []

    root = Path("/workspace/my-app")

    for file in root.rglob("*.py"):
        try:
            content = file.read_text(
                encoding="utf-8"
            )

            if query.lower() in content.lower():
                results.append(str(file))

        except (OSError, UnicodeDecodeError):
            continue

    return results

The distinction is clear:

project://readme
        ↓
Resource
        ↓
Information

search_code("payment")
        ↓
Tool
        ↓
Operation

This is one of the most useful practical examples of MCP Roots vs Resources vs Tools.

Connecting a Root to Filesystem Access

A production implementation should not simply hard-code:

root = Path("/workspace/my-app")

Instead, the server should establish an appropriate relationship between the client-provided Root and the filesystem operations it exposes.

The conceptual flow is:

Client Root
     ↓
file:///workspace/my-app
     ↓
Server validates allowed location
     ↓
Resource / Tool request
     ↓
Path validation
     ↓
Filesystem operation

This creates a much stronger architecture than allowing arbitrary paths.

Preventing Path Traversal

A Tool that accepts file paths should protect against traversal attacks.

For example, this input is suspicious:

../../../../etc/passwd

A safer implementation resolves the requested path and verifies that it remains inside the allowed directory.

from pathlib import Path


def validate_path(root: Path, requested: str) -> Path:
    root = root.resolve()
    target = (root / requested).resolve()

    if target != root and root not in target.parents:
        raise PermissionError(
            "Requested path is outside the allowed root"
        )

    return target

The important security principle is:

Never trust the requested path.
Resolve it.
Validate it.
Then access it.

The Root provides useful context, but server-side validation remains necessary.

Resource Access vs Tool Execution

Consider an AI agent investigating a failing test.

It may first read:

project://architecture

Then:

project://config

Then invoke:

search_code("payment_timeout")

Then invoke:

run_tests("tests/payment")

The workflow is:

Resource
   ↓
Understand system

Resource
   ↓
Understand configuration

Tool
   ↓
Search implementation

Tool
   ↓
Execute tests

This separation gives the AI a more understandable capability model.

Building a Tool With Controlled Input

Tools should expose structured inputs rather than unrestricted commands whenever possible.

Instead of:

@mcp.tool()
def execute(command: str):
    ...

use a specific capability:

@mcp.tool()
def run_tests(scope: str) -> dict:
    """Run an approved test scope."""

    allowed_scopes = {
        "unit": "tests/unit",
        "integration": "tests/integration",
        "payment": "tests/payment",
    }

    if scope not in allowed_scopes:
        raise ValueError("Unsupported test scope")

    # Test execution would occur here.

    return {
        "scope": scope,
        "status": "started"
    }

The Tool now communicates a clear contract.

The model cannot simply supply an arbitrary shell command.

Comparing Generic and Specialized Tools

DesignExampleFlexibilityRisk
Generic commandexecute(command)Very HighVery High
File operationread_file(path)HighMedium
Search operationsearch_code(query)FocusedLow–Medium
Test operationrun_tests(scope)FocusedLow–Medium
Business operationcreate_issue(...)FocusedMedium

The specialized Tool is generally easier to validate and secure.

Testing MCP Resources

Resources should also be tested.

For a Resource:

project://readme

test at least:

Resource exists
URI is correct
Content is retrievable
Content is valid
Expected MIME type is provided
Errors are handled correctly

A simple application-level test might look like:

def test_project_readme():
    content = project_readme()

    assert isinstance(content, str)
    assert "# Payment Service" in content
    assert len(content) > 0

The exact testing strategy depends on the MCP SDK and server architecture.

Testing MCP Tools

Tools need different tests because they execute behavior.

For:

search_code("payment")

test:

Valid query
Empty query
Unknown query
Large query
Special characters
Expected results
No-result behavior
Filesystem errors

Example:

def test_search_code():
    results = search_code("payment")

    assert isinstance(results, list)

Then add behavior-specific assertions.

The testing difference is important:

Resource testing
      ↓
Can I retrieve correct information?

Tool testing
      ↓
Does the operation behave correctly?

Testing Root Boundaries

Root-related testing should focus heavily on boundaries.

Suppose the allowed Root is:

/workspace/my-app

Valid:

/workspace/my-app/src/app.py

Invalid:

/workspace/other-app/app.py

Also invalid:

/etc/passwd

and:

/workspace/my-app/../secrets

A test can verify that these paths are rejected.

import pytest
from pathlib import Path


def test_path_outside_root_is_rejected():
    root = Path("/workspace/my-app")

    with pytest.raises(PermissionError):
        validate_path(
            root,
            "../secrets/config.json"
        )

Security testing should be treated as part of MCP testing rather than something added later.

Testing Tool Authorization

Suppose the server provides:

delete_project_file(path)

The test suite should verify:

Authorized request
    ↓
Allowed

Unauthorized request
    ↓
Rejected

Invalid path
    ↓
Rejected

Sensitive path
    ↓
Rejected

Example:

def test_delete_requires_authorization():
    user = create_test_user(
        permissions=[]
    )

    result = attempt_delete(
        user=user,
        path="src/app.py"
    )

    assert result.status == "forbidden"

The exact implementation depends on the authentication and authorization system surrounding the MCP server.

MCP Roots vs Resources vs Tools in Security Testing

Security testing becomes clearer when the three concepts are separated.

MCP ConceptMain Security Question
RootIs the requested scope allowed?
ResourceIs the data safe to expose?
ToolIs the action authorized and validated?

For example:

Root
→ Can this project location be accessed?

Resource
→ Should this configuration data be exposed?

Tool
→ Is this user allowed to modify the configuration?

This creates three distinct security questions.

Sensitive Resources

Resources can accidentally expose sensitive information.

Consider:

project://environment

If it contains:

DATABASE_PASSWORD=...
API_KEY=...
SECRET_TOKEN=...

then exposing it directly to an AI model could create a serious data-leakage problem.

A safer design may provide a sanitized Resource:

{
  "database": "payments",
  "environment": "staging",
  "debug": true
}

instead of:

DATABASE_PASSWORD=secret
API_KEY=secret

The principle is:

Resource availability
≠
Unlimited data exposure

Sensitive Tool Operations

Tools require even stronger consideration when they modify external systems.

Consider:

deploy_production()

This is substantially more dangerous than:

get_deployment_status()

The first changes system state.

The second retrieves information.

A capability matrix can make this distinction explicit:

OperationTypeState ChangeRisk
Read deployment statusResourceNoLow
Search deployment logsResource/ToolNoMedium
Restart serviceToolYesHigh
Deploy productionToolYesCritical

This is why MCP Roots vs Resources vs Tools should be considered part of security architecture, not merely API design.

Designing Idempotent Tools

Where possible, Tools should have predictable behavior.

For example:

get_status()

is naturally read-oriented.

But:

create_resource()

may produce a new object each time.

Repeated invocation could create:

Resource A
Resource B
Resource C

If duplicate operations are dangerous, the Tool may need idempotency controls.

For example:

@mcp.tool()
def create_ticket(
    title: str,
    request_id: str
) -> dict:
    ...

The server could use request_id to detect duplicate requests.

This becomes important when AI agents retry operations.

Tool Errors Should Be Meaningful

Avoid returning vague results:

Error

Prefer structured information:

{
  "status": "error",
  "code": "INVALID_SCOPE",
  "message": "The requested test scope is not supported."
}

A structured error gives the AI more information for reasoning.

For example:

Tool failed
    ↓
INVALID_SCOPE
    ↓
AI understands why
    ↓
AI can select another valid scope

Good error design therefore improves both debugging and agent behavior.

Resource Errors Should Also Be Clear

Suppose:

project://architecture

cannot be loaded.

A useful error should distinguish:

RESOURCE_NOT_FOUND

from:

RESOURCE_ACCESS_DENIED

and:

RESOURCE_UNAVAILABLE

These represent different conditions.

Not found
    ≠
Not authorized
    ≠
Temporarily unavailable

That distinction matters when an AI agent decides what to do next.

Observability for MCP Servers

A production MCP server should provide useful logging.

At minimum, consider recording:

Timestamp
Client/session identifier
Capability used
Resource URI
Tool name
Execution duration
Success/failure
Error category

Avoid logging sensitive arguments or secrets.

A useful conceptual log entry might be:

{
  "event": "tool_execution",
  "tool": "run_tests",
  "scope": "unit",
  "status": "success",
  "duration_ms": 1240
}

This makes troubleshooting significantly easier.

Measuring Tool Performance

Tool execution time can become important when an AI agent performs multiple operations.

Suppose:

search_code() → 100 ms
read_resource() → 50 ms
run_tests() → 12,000 ms

The expensive operation is obvious.

Observability can help identify where latency originates.

The agent workflow becomes:

AI Request
   ↓
Resource lookup
   ↓
Tool execution
   ↓
Result
   ↓
Next reasoning step

If every Tool takes several seconds, agent interactions can become slow.

Avoiding Unnecessary Tool Calls

A well-designed AI agent should not call a Tool when a Resource already contains the required information.

For example, if:

project://architecture

already describes the project structure, there may be no reason to call:

get_project_structure()

again.

A useful design principle is:

Use existing information first.
Execute an operation only when necessary.

This can reduce latency, cost, and unnecessary system interaction.

Resource-First Reasoning

A useful agent workflow can be:

User Question
     ↓
Check available Resources
     ↓
Retrieve relevant information
     ↓
Reason about the request
     ↓
Determine whether a Tool is required
     ↓
Invoke Tool
     ↓
Evaluate result

For example:

User:
"Why is the payment test failing?"

The agent might:

1. Read architecture Resource
2. Read test documentation Resource
3. Search source code using a Tool
4. Run the relevant tests using a Tool
5. Analyze the output

The distinction between information retrieval and action allows the agent to reason more deliberately.

Learning Exercise: Design an MCP Capability Set

Create an MCP server for a QA automation project.

Requirements:

Read test documentation
Read test configuration
Search test code
Run tests
Generate a test report
Delete temporary reports

Design the capabilities.

A reasonable answer is:

Root:
file:///workspace/qa-project

Resources:
qa://documentation
qa://configuration

Tools:
search_tests(query)
run_tests(scope)
generate_report()
delete_temp_report(path)

Now identify the security-sensitive capability:

delete_temp_report(path)

That Tool should have stricter path validation than:

qa://documentation

This exercise demonstrates how capability type influences implementation and security requirements.

A More Advanced Example

Imagine an MCP server for a CI/CD platform.

Resources:

deployment://production/status
deployment://staging/status
pipeline://latest

Tools:

run_pipeline()
rollback_deployment()
approve_deployment()

The AI agent might reason:

Read production status
        ↓
Resource

Read latest pipeline
        ↓
Resource

Determine failure
        ↓
AI reasoning

Run pipeline
        ↓
Tool

If the agent wants to perform:

rollback_deployment()

the Tool should enforce authorization and potentially additional safety controls.

This is a practical example of why the distinction between MCP Roots vs Resources vs Tools becomes increasingly important in production environments.

Designing Human Approval Boundaries

Some Tools are too consequential to execute without additional controls.

For example:

delete_database()
deploy_production()
rotate_credentials()
shutdown_service()

A safer architecture may introduce:

AI
 ↓
Tool Request
 ↓
Policy Check
 ↓
Human Approval
 ↓
Execution

This allows AI agents to remain useful without giving them unrestricted authority over critical systems.

Capability Design by Risk

A useful strategy is to classify capabilities:

Level 1 — Read
Resources and low-risk queries

Level 2 — Analyze
Search and diagnostic Tools

Level 3 — Modify
Configuration and data changes

Level 4 — Critical
Deployment, deletion, security changes

The higher the level, the stronger the controls should become.

LevelCapabilityControls
1Read documentationBasic access
2Search/run diagnosticsInput validation
3Modify dataAuthorization + auditing
4Production changesAuthorization + policy + approval

This risk-based approach scales much better than treating every MCP capability identically.

MCP Capability Design Review

Before adding a capability, document it like this:

Name:
Purpose:
Type:
Inputs:
Outputs:
Data accessed:
State changed:
Required permissions:
Failure modes:
Audit requirements:
Risk level:

For example:

Name:
run_tests

Purpose:
Execute approved automated tests

Type:
Tool

Inputs:
scope

Outputs:
test result summary

Data accessed:
Project workspace

State changed:
Temporary test artifacts

Required permissions:
Test execution

Risk level:
Medium

This makes MCP server capabilities easier to review before implementation.

Practical Comparison: Poor vs Strong MCP Design

AreaPoor DesignStrong Design
FilesystemArbitrary pathsRoot-scoped paths
DataEverything exposedCurated Resources
ActionsGeneric command executionSpecialized Tools
InputsUnrestricted stringsValidated schemas
Errors"Error"Structured error information
SecurityTrust the clientServer-side validation
LoggingMinimalAuditable events
PermissionsBroad accessLeast privilege
Destructive actionsImmediateControlled execution
TestingHappy path onlyFunctional + security boundaries

The strongest MCP implementations are not necessarily the largest.

They are the ones with the clearest boundaries.

A Complete MCP Capability Flow

Putting everything together:

                    AI Agent
                       │
                       ↓
                  MCP Client
                       │
              ┌────────┼────────┐
              │        │        │
             Root   Resources  Tools
              │        │        │
              ↓        ↓        ↓
            Scope     Data     Actions
                       │        │
                       └────┬───┘
                            ↓
                       MCP Server
                            │
                  ┌─────────┼─────────┐
                  ↓         ↓         ↓
              Filesystem  Database  APIs

The MCP server acts as the controlled boundary between the AI system and external capabilities.

The Root helps establish context.

Resources provide information.

Tools provide executable capabilities.

The server remains responsible for validating and enforcing the actual security boundaries.

Advanced Learning: Think in Capabilities, Not Functions

A beginner may design an MCP server around functions:

read_file()
write_file()
execute_command()
query_database()

A stronger MCP engineer thinks about capabilities:

What does the AI actually need to accomplish?

What information does it need?

What actions are necessary?

What actions are unnecessary?

What is the minimum privilege required?

That leads to a more intentional design:

Project Context
      ↓
Relevant Resources
      ↓
Specific Tools
      ↓
Validated Operations
      ↓
Controlled Results

This capability-oriented mindset is one of the most important skills for building reliable AI agent infrastructure with MCP.

Preparing a Production MCP Server

Before moving an MCP server toward production, verify:

✓ Roots are clearly scoped
✓ Resource URIs are meaningful
✓ Sensitive Resources are filtered
✓ Tools have narrow responsibilities
✓ Tool inputs are validated
✓ Authorization is enforced server-side
✓ File paths are validated
✓ Destructive actions have additional controls
✓ Errors are structured
✓ Logs are useful but do not leak secrets
✓ Resources and Tools have automated tests
✓ Security boundaries are tested
✓ Capability risk has been documented

A server that satisfies these principles is far easier to reason about, test, secure, and operate.

The real power of MCP Roots vs Resources vs Tools comes from combining them deliberately rather than treating them as interchangeable ways of exposing functionality.

Applying MCP Roots, Resources, and Tools in Production

The distinction between MCP Roots vs Resources vs Tools becomes most valuable when designing an MCP server that must operate reliably in real development, testing, and production environments.

The goal is not simply to make every capability available to an AI agent.

The goal is to expose the right capability through the right MCP primitive, with clear boundaries around data, actions, permissions, and failure handling.

A Production MCP Capability Model

A practical production architecture can be represented as:

                    AI Agent
                       │
                       ▼
                  MCP Client
                       │
          ┌────────────┼────────────┐
          │            │            │
        Roots       Resources      Tools
          │            │            │
       Scope          Data        Actions
          │            │            │
          └────────────┼────────────┘
                       ▼
                  MCP Server
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
      Filesystem    Database       APIs

Each layer has a different responsibility.

Root
→ Establishes relevant scope

Resource
→ Makes information available

Tool
→ Executes a capability

This separation should remain visible in the server design, documentation, testing strategy, and security model.

Building a Complete MCP Project Server

Consider a project assistant that needs to understand and operate on a software repository.

The requirements are:

Read project documentation
Read configuration
Search source code
Run tests
Create issues

A clean MCP design could be:

Root:
file:///workspace/payment-service

Resources:
project://readme
project://architecture
project://configuration

Tools:
search_code(query)
run_tests(scope)
create_issue(title, description)

The AI now has an explicit capability map.

WHERE
  ↓
Root

WHAT INFORMATION
  ↓
Resources

WHAT ACTION
  ↓
Tools

This is the central architectural pattern behind MCP Roots vs Resources vs Tools.

Designing the Server Around Capabilities

A common mistake is to start with implementation functions:

def read_file():
    ...

def write_file():
    ...

def execute_command():
    ...

def query_database():
    ...

and then expose all of them through MCP.

A stronger approach starts with questions:

What does the AI need to know?

What does the AI need to do?

What must the AI never be allowed to do?

This produces a capability-oriented design.

For example:

Required:
Read architecture
Search code
Run tests

Not required:
Delete arbitrary files
Execute arbitrary shell commands
Modify production database
Deploy production

The resulting MCP server is smaller, safer, and easier for an AI model to understand.

Production Resource Design

Resources should provide useful information without unnecessarily exposing sensitive internal data.

Consider a configuration file:

DATABASE_HOST=db.internal
DATABASE_USER=app
DATABASE_PASSWORD=secret
API_TOKEN=secret
DEBUG=true

Exposing the entire configuration as:

project://configuration

could be dangerous.

A safer Resource might expose:

{
  "environment": "staging",
  "database_host": "db.internal",
  "debug": true
}

while removing:

DATABASE_PASSWORD
API_TOKEN
PRIVATE_KEY
SECRET_TOKEN

The principle is:

Resource
    ↓
Relevant information
    ↓
Sanitized data
    ↓
AI context

Not:

Resource
    ↓
Dump everything

Production Tool Design

Tools should have focused responsibilities.

Instead of:

@mcp.tool()
def execute_command(command: str):
    ...

prefer:

@mcp.tool()
def run_tests(scope: str) -> dict:
    """Run an approved test scope."""

    allowed_scopes = {
        "unit",
        "integration",
        "payment"
    }

    if scope not in allowed_scopes:
        raise ValueError("Unsupported test scope")

    return {
        "scope": scope,
        "status": "started"
    }

The Tool now has a clear boundary.

The AI can request:

run_tests("unit")

but it cannot transform the Tool into an unrestricted shell.

Comparing MCP Capability Designs

DesignFlexibilityControlSecurityAI Understanding
Generic command ToolVery HighLowLowMedium
Generic file ToolHighMediumMediumMedium
Specialized search ToolMediumHighHighHigh
Specialized test ToolMediumHighHighHigh
Domain-specific business ToolFocusedHighHighHigh

A powerful MCP server is not necessarily one with the largest number of capabilities.

A strong MCP server exposes precise capabilities.

Testing the Three MCP Primitives

Testing MCP Roots vs Resources vs Tools should not use a single testing strategy.

Each primitive requires different validation.

Testing Roots

Verify:

Correct URI
Correct project scope
Invalid paths rejected
Traversal attempts rejected
Unauthorized locations rejected

Example:

def test_root_boundary():
    root = Path("/workspace/project")

    valid = validate_path(
        root,
        "src/app.py"
    )

    assert valid.name == "app.py"

And:

def test_root_blocks_traversal():
    root = Path("/workspace/project")

    with pytest.raises(PermissionError):
        validate_path(
            root,
            "../secrets/key.txt"
        )

Testing Resources

Verify:

Resource exists
URI is correct
Content is correct
Sensitive information is removed
Expected format is returned
Unavailable data produces useful errors

Example:

def test_readme_resource():
    content = project_readme()

    assert "# Payment Service" in content
    assert "PASSWORD" not in content

Testing Tools

Verify:

Input validation
Authorization
Business rules
Execution behavior
Error handling
Side effects
Audit logging

Example:

def test_run_tests_rejects_invalid_scope():
    with pytest.raises(ValueError):
        run_tests("arbitrary-command")

This separation makes MCP testing much more effective.

Security Testing MCP Tools

Tools are often the highest-risk MCP capability because they can execute operations.

Consider:

create_issue()
update_config()
delete_file()
deploy_application()

The server should evaluate:

Who requested it?

What arguments were supplied?

Is the operation permitted?

Does the operation affect sensitive data?

Does the operation modify state?

Should human approval be required?

A useful security pipeline is:

Tool Request
     ↓
Schema Validation
     ↓
Authentication
     ↓
Authorization
     ↓
Business Validation
     ↓
Policy Check
     ↓
Execution
     ↓
Audit

The AI model should never be treated as a trusted security boundary.

Designing High-Risk Tools

Consider:

deploy_production()

This is very different from:

get_deployment_status()

The first changes production state.

The second reads information.

A production system may therefore classify them differently:

ToolState ChangeRiskAdditional Control
get_deployment_status()NoLowAccess control
run_pipeline()YesMediumAuthorization
rollback_deployment()YesHighAuthorization + audit
deploy_production()YesCriticalPolicy + approval

This risk-based approach is more useful than treating every Tool equally.

Human Approval for Critical Operations

Some MCP Tools should not execute immediately after an AI-generated request.

For example:

delete_database()
deploy_production()
rotate_credentials()
shutdown_service()

A safer workflow is:

AI
 ↓
Tool Request
 ↓
Policy Engine
 ↓
Human Approval
 ↓
Tool Execution
 ↓
Audit Log

This allows AI agents to participate in important workflows while maintaining a human-controlled boundary around critical actions.

Designing MCP Resources for AI Context

Resources should also be designed with model context in mind.

A Resource containing 500,000 lines of raw logs is technically useful but may be practically poor for an AI agent.

Instead of:

logs://production/all

consider more targeted resources such as:

logs://production/errors
logs://production/recent
logs://production/payment

The objective is to expose information that is:

Relevant
Structured
Understandable
Discoverable

This reduces unnecessary context and makes agent reasoning more efficient.

Resource vs Tool for Dynamic Data

Dynamic data can sometimes create confusion.

Suppose an application needs the current deployment status.

You might expose:

deployment://production/status

as a Resource.

That is appropriate when the primary requirement is to retrieve current information.

But if the application needs:

restart_service()

that is an executable operation and should be represented as a Tool.

The distinction is:

Current deployment status
        ↓
Resource

Restart deployment
        ↓
Tool

The fact that both interact with the same underlying system does not make them the same MCP primitive.

When a Tool Reads Data

A Tool can also retrieve information.

For example:

search_code(query)

may return source-code matches.

That does not make every read operation a Resource.

The better distinction is based on the capability being exposed.

Resource
→ Represents accessible information

Tool
→ Represents an executable operation

Therefore:

project://architecture

can be a Resource.

While:

search_code("authentication")

can remain a Tool because the search itself is an executable operation.

This distinction prevents overly simplistic MCP architecture rules.

A Complete AI Agent Workflow

Consider this request:

Find why the payment tests are failing and create an issue if the failure is confirmed.

A well-designed MCP agent could perform:

1. Read project architecture
        ↓
   Resource

2. Read test configuration
        ↓
   Resource

3. Search payment implementation
        ↓
   Tool

4. Run payment tests
        ↓
   Tool

5. Analyze test output
        ↓
   AI reasoning

6. Create issue
        ↓
   Tool

The complete workflow is:

Resources
   ↓
Context
   ↓
Tools
   ↓
Evidence
   ↓
AI reasoning
   ↓
Tool
   ↓
External action

This is a powerful example of how MCP Roots vs Resources vs Tools work together rather than independently.

Designing for Observability

Production MCP servers should make capability execution observable.

Useful events include:

{
  "event": "tool_execution",
  "tool": "run_tests",
  "scope": "payment",
  "status": "success",
  "duration_ms": 1834
}

For Resources:

{
  "event": "resource_access",
  "uri": "project://architecture",
  "status": "success"
}

For security events:

{
  "event": "authorization_denied",
  "tool": "deploy_production",
  "reason": "missing_permission"
}

Logs should never unnecessarily contain:

Passwords
API keys
Tokens
Private keys
Sensitive personal information

Observability should improve debugging without creating another data-leakage channel.

Designing Useful Error Responses

An MCP server should distinguish different failure conditions.

Instead of:

Error

return structured information where appropriate:

{
  "status": "error",
  "code": "INVALID_SCOPE",
  "message": "The requested test scope is not supported."
}

This allows the AI agent to understand whether it should:

Retry
Change input
Request permission
Use another capability
Report failure

Different failures should remain distinguishable:

NOT_FOUND
ACCESS_DENIED
INVALID_INPUT
TIMEOUT
UNAVAILABLE
EXECUTION_FAILED

Good error design improves both debugging and agent reasoning.

Idempotency and Agent Retries

AI agents may retry operations.

Consider:

create_issue()

If the first request succeeds but the response is lost, the agent may attempt the same operation again.

Without protection:

Issue #101
Issue #102

could be created for the same request.

A Tool can accept an idempotency key:

@mcp.tool()
def create_issue(
    title: str,
    description: str,
    request_id: str
) -> dict:
    ...

The server can use request_id to detect duplicate operations.

This is particularly important for state-changing Tools.

Performance Considerations

An AI agent may execute multiple MCP operations for a single user request.

For example:

Resource lookup     50 ms
Resource lookup     40 ms
Search Tool        120 ms
Test Tool         2400 ms
Issue Tool          180 ms

The total interaction time can grow quickly.

Good MCP design therefore considers:

Resource efficiency
Tool latency
Result size
Unnecessary calls
Caching
Timeouts

The agent should not repeatedly request information it already has.

Resource-First Agent Strategy

A useful strategy is:

User Request
     ↓
Check available context
     ↓
Read relevant Resources
     ↓
Determine missing information
     ↓
Call Tools only when necessary
     ↓
Evaluate results
     ↓
Perform final action

For example:

"Explain why the deployment failed."

        ↓

Read deployment status Resource

        ↓

Read recent deployment logs Resource

        ↓

If evidence is insufficient:

Search deployment configuration Tool

        ↓

Analyze evidence

This reduces unnecessary Tool execution.

Capability Review Before Production

Before exposing an MCP capability, document:

Capability name
Purpose
Primitive type
Inputs
Outputs
Data accessed
State changed
Required permissions
Failure modes
Security risks
Audit requirements

For example:

Capability:
run_tests

Type:
Tool

Purpose:
Execute approved automated tests

Input:
scope

Output:
test summary

State change:
Temporary test artifacts

Permission:
test:execute

Risk:
Medium

Audit:
Required

This documentation becomes extremely useful as the MCP server grows.

Final MCP Architecture Example

A production-oriented repository MCP server could look like:

MCP Repository Server
│
├── Roots
│   └── file:///workspace/project
│
├── Resources
│   ├── project://readme
│   ├── project://architecture
│   ├── project://configuration
│   └── project://test-documentation
│
├── Tools
│   ├── search_code(query)
│   ├── run_tests(scope)
│   ├── generate_report()
│   └── create_issue(title, description)
│
├── Security
│   ├── Path validation
│   ├── Authorization
│   ├── Input validation
│   └── Audit logging
│
└── Observability
    ├── Tool execution logs
    ├── Resource access logs
    ├── Errors
    └── Performance metrics

This architecture is significantly more maintainable than exposing one generic Tool with unrestricted access to the underlying system.

MCP Roots vs Resources vs Tools: Final Decision Framework

When designing a new MCP capability, ask:

Is it defining scope?

Yes → Root

Is it exposing information?

Yes → Resource

Is it providing an executable capability?

Yes → Tool

Then ask the production questions:

Is the data sensitive?

Can the action modify state?

What permissions are required?

Can the input be abused?

Can the capability be narrowed?

Does the AI actually need it?

Should the action require approval?

How will the operation be tested?

How will the operation be audited?

This turns MCP capability design into an engineering discipline rather than simply registering functions with a server.

Internal Links

External Links

AI Overview Optimization

MCP Roots define the scope of an MCP client’s working environment, MCP Resources provide accessible information and context, and MCP Tools expose executable capabilities. Roots answer where an agent can operate, Resources answer what information it can access, and Tools answer what actions it can perform.

MCP
├── Roots
│   └── Define scope
├── Resources
│   └── Provide information
└── Tools
    └── Execute actions

People Asked Questions

What are MCP Roots?

MCP Roots define the relevant scope or filesystem locations associated with an MCP client and its work context.

What are MCP Resources?

MCP Resources expose information that an MCP client or AI application can retrieve and use as context.

What are MCP Tools?

MCP Tools expose executable capabilities that allow an AI application to perform operations through an MCP server.

What is the difference between MCP Roots, Resources, and Tools?

Roots establish scope, Resources provide information, and Tools provide executable capabilities. They work together but serve different architectural purposes.

Are MCP Tools more dangerous than MCP Resources?

Tools can introduce greater operational risk because they can execute actions or modify external systems. Their inputs, permissions, and side effects should therefore be carefully controlled.

Should MCP Tools execute arbitrary shell commands?

Generally, unrestricted command execution should be avoided when a narrower, purpose-built Tool can provide the required capability with better validation and security.

Conclusion: Mastering MCP Roots vs Resources vs Tools

MCP Roots vs Resources vs Tools is ultimately about creating clear boundaries between scope, information, and action.

Roots answer:

Where should this MCP client operate?

Resources answer:

What information can the AI access?

Tools answer:

What operations can the AI perform?

A production MCP architecture should combine all three deliberately:

Root
 ↓
Controlled Scope
 ↓
Resources
 ↓
Relevant Context
 ↓
AI Reasoning
 ↓
Tools
 ↓
Validated Action
 ↓
Audited Result

The biggest lesson is that MCP is not about giving an AI agent maximum access.

It is about giving the agent the right access, through the right interface, with the right controls.

When Roots establish meaningful boundaries, Resources provide curated information, and Tools expose narrowly defined capabilities, MCP servers become easier to understand, test, secure, and scale.

That is the foundation for building production-grade MCP systems rather than experimental AI integrations.


Continue Your MCP Zero to Hero Journey

From here onward, every lesson will build on this environment as we move from concepts to production-ready implementations.

Enjoyed this article? Explore more in-depth guides on AI engineering, automation testing, Model Context Protocol, Playwright, and intelligent software quality at www.skakarh.com. Follow QAPulse by SK for practical, production-focused tutorials designed for QA engineers, SDETs, and AI developers.

Frequently Asked Questions

What is the fundamental difference between MCP Roots, Resources, and Tools?
MCP Roots define where the client allows access, providing scope. MCP Resources expose data for the client or model to read, while MCP Tools provide actions the model can execute. The simplest distinction between them is based on their primary purpose.
Why is understanding the distinction between MCP Roots, Resources, and Tools important for building secure MCP systems?
Understanding this distinction is important because mixing these concepts can lead to an MCP implementation that is unnecessarily powerful, difficult to secure, and harder for an AI model to reason about. This distinction matters when designing secure MCP applications. For example, a controlled Root provides an important contextual boundary.
What is the primary function of an MCP Root in an AI application?
MCP Roots provide a way for an MCP client to communicate filesystem locations that are relevant to the current context. The important idea is that a Root is primarily about scope and establishing a permitted location, not about performing an operation. This creates an important contextual boundary, especially when working with local project files.
Advertisement
Found this helpful? Clap to let Shahnawaz know — you can clap up to 50 times.