跪拜 Guibai
← Back to the summary

MCP Resources Don't Auto-Feed the Model — Here's the Explicit 4-Step Chain

MCP Server can provide not only Tools but also Resources.

Tools are easy to understand: when a model needs to query a user, read a file, or execute a command, it initiates a tool call, and the Runtime executes it and returns the result to the model.

Resources look more like "materials": usage guides, configuration instructions, database structures, project documentation — all can be exposed through an MCP Server.

But there is a very common misunderstanding here:

Once a Server registers a Resource, the model can automatically see it.

In reality, registration only means this resource can be discovered and read by the MCP Client. When it is read and which content enters the Prompt is still decided by the Host application.

This article combines a minimal Demo to completely break down the following chain:

registerResource
→ listResources
→ readResource
→ SystemMessage
→ LLM

Tools and Resources in the Demo

This MCP Server registers two capabilities simultaneously:

Tool: query_user
Resource: docs://guide

query_user receives a user ID and queries user information from a simulated database.

docs://guide provides the usage guide for the MCP Server.

Although they come from the same Server, they enter the model's context in different ways:

Capability Who decides to use it How it enters the context
Tool Usually decided by the model based on the task Runtime wraps the execution result into a ToolMessage
Resource The Host application decides which content to read The application actively places it into messages after reading

You can remember this one sentence first:

Tools wait for the model to initiate a call, Resources wait for the application to actively read.

Step 1: Registering a Resource in the MCP Server

The Server uses registerResource() to register the usage guide:

server.registerResource(
  'Usage Guide',
  'docs://guide',
  {
    description: 'Usage Guide',
    mimeType: 'text/plain',
  },
  async () => {
    return {
      contents: [
        {
          uri: 'docs://guide',
          mimeType: 'text/plain',
          text: `
            MCP Server Usage Guide
            Function: Provides tools such as user query.
            Usage: Converse in natural language within the MCP Client,
            and the Client will call the corresponding tools.
          `,
        },
      ],
    };
  }
);

There are four key parts here:

docs://guide is not necessarily a URL that can be opened in a browser.

It is more like a resource identifier used internally by MCP: the Client first discovers the resource via the URI, then uses this URI to request the actual content.

Successful Registration Does Not Mean the Model Has Seen It

When the Server finishes executing registerResource(), it has only completed:

"I have a docs://guide here, which can be read via MCP."

It has not yet completed:

"Send the body of docs://guide to the large language model."

When the model calls the interface, what it can truly see are the messages in the current request, the bound Tools, and other explicit contexts.

If the Host has not read the Resource and has not placed the content into messages, the model will not automatically know about this material just because it exists in the MCP Server.

Why not just stuff all Resources into the model by default?

Therefore, the selection and injection of Resources must remain under application-layer control.

Step 2: The Client Discovers Available Resources

First, create a MultiServerMCPClient in the Host to connect to the local MCP Server:

const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: 'node',
      args: ['/project-path/my-mcp-server.mjs'],
    },
  },
});

The local Server will start as a child process, and the Client communicates with it via stdio.

Next, call:

const resources = await mcpClient.listResources();

listResources() gets the resource list, answering the question:

What Resources does the currently connected MCP Server provide?

The returned results are organized by Server, so the code needs to iterate through serverName and the corresponding resource list:

for (const [serverName, resources] of Object.entries(res)) {
  for (const resource of resources) {
    // Next step: read the resource
  }
}

This step is still just discovering resources; the body of docs://guide has not yet been obtained.

Step 3: Reading Resource Content via URI

After discovering the resource, call readResource():

const content = await mcpClient.readResource(
  serverName,
  resource.uri
);

Two pieces of information are needed here:

serverName: which MCP Server the resource comes from
resource.uri: which specific Resource to read

Why can't you just pass the URI?

Because a single Client can connect to multiple MCP Servers simultaneously. The Host needs to know both the resource identifier and which Server to send the read request to.

The content returned by the Server may contain multiple entries, so the Demo continues iterating:

let resourcesContent = '';

for (const [serverName, resources] of Object.entries(res)) {
  for (const resource of resources) {
    const content = await mcpClient.readResource(
      serverName,
      resource.uri
    );

    for (const item of content) {
      if (item.text) {
        resourcesContent += item.text;
      }
    }
  }
}

At this point, the Host has truly obtained the text content of the Resource.

Step 4: Placing the Resource into a SystemMessage

After obtaining resourcesContent, the application actively creates a SystemMessage:

const messages = [
  new SystemMessage(resourcesContent),
  new HumanMessage(query),
];

Only now does the Resource truly enter the model's context.

The complete process is:

MCP Server registers the Resource
  ↓
Host calls listResources() to discover resources
  ↓
Host calls readResource() to read the body
  ↓
Host filters and organizes resource content
  ↓
Host creates a SystemMessage
  ↓
LLM sees the Resource in messages

This Demo directly concatenates all text content, suitable for a minimal example with very few resources.

Real projects typically also need to filter resources based on URI, name, user permissions, and the current task, and cannot blindly stuff all content into the Prompt.

Why Doesn't a Resource Need a ToolMessage?

ToolMessage is used to return the result of a Tool Call already initiated by the model, so it must carry the corresponding tool_call_id.

The flow for a Resource is different.

It is read by the Host and placed into the SystemMessage before the first model call:

SystemMessage(Resource content)
HumanMessage(User question)
→ LLM

The model did not first generate a Resource Call, and the Runtime did not execute a Tool Call, so a ToolMessage is not needed here.

If the user asks:

What is the MCP Server usage guide?

The model can answer directly based on the guide in the SystemMessage.

If the user instead asks:

Query the user information for ID 001.

The model then needs to initiate a query_user Tool Call, and the Runtime returns the query result via ToolMessage after execution.

Within the same MCP Server, both chains can exist simultaneously:

Resource: Host reads → SystemMessage → LLM
Tool: LLM initiates → Runtime executes → ToolMessage → LLM

What is the Difference Between Resource and RAG?

Both Resources and RAG can supplement the model with external knowledge, but the handling methods differ.

The usage guide in the current Demo is very short and can be placed into the context all at once:

Read the complete Resource → Place into SystemMessage

If the materials become hundreds of documents and hundreds of thousands of text segments, it is no longer suitable to stuff them all into the Prompt. At this point, it is more appropriate to first build a vector index and then retrieve only relevant segments based on the question:

User question → RAG retrieves relevant segments → Place into Prompt

It can be differentiated like this:

Scenario More Suitable Approach
Short, stable instructions needed every time Directly read MCP Resource
Large volume of documents, need to find relevant segments by question RAG retrieval
Need to execute queries, write files, or call APIs MCP Tool

Resource is not a replacement for RAG, and RAG is not a replacement for Resource. They solve context problems at different scales and with different control methods.

Still Need to Close the Client After the Task Ends

This Demo starts the local MCP Server child process via command + args.

After completing the task, the connection needs to be closed:

await mcpClient.close();

Otherwise, the child process and stdio communication channel may still exist, causing the Node.js script to keep running.

A more robust way is to put it inside finally:

try {
  await runAgentWithTools(query);
} finally {
  await mcpClient.close();
}

Whether the model answers successfully or encounters an error midway, it can release the MCP connection and child process.

Summary

registerResource() is only responsible for declaring a resource in the MCP Server and does not automatically modify the model's context.

For a Resource to truly enter the model, the Host must explicitly complete four steps:

listResources()
→ readResource(serverName, uri)
→ Organize resource text
→ SystemMessage(resourcesContent)

This also explains the core control relationship of MCP Resources:

The Server is responsible for providing resources
The Client is responsible for discovering and reading resources
The Host is responsible for filtering and injecting context
The LLM is responsible for understanding the content and generating answers

The model will not automatically read all materials, nor should it automatically obtain all materials. What content to put into the context, when to put it, and who is allowed to read it ultimately remains an engineering problem that the AI application is responsible for.