NEWn1n v2.0.1 is live! Enterprise Unified LLM API Gateway with 500+ AI Models, up to 90% off,Try now

Teaching LLMs to Request Model Context Protocol Resources and Prompts on Demand

Authors
  • avatar
    Name
    Nino
    Occupation
    Senior Tech Editor

Managing the context window of large language models (LLMs) is one of the most critical challenges in building production-ready AI agents. As models like Claude 3.5 Sonnet and DeepSeek-V3 become more integrated into developer workflows, they require access to external databases, API documentation, and codebase structures. The Model Context Protocol (MCP), open-sourced by Anthropic, has emerged as a powerful standard for exposing these capabilities to LLMs.

However, standard integrations of MCP often suffer from a fundamental design flaw: they flood the LLM's context window with unnecessary information. When using n1n.ai, the premier LLM API aggregator, developers have access to high-performance, low-latency models that can process vast amounts of tokens. But just because a model can process a 200k token context window doesn't mean it should. Flooding the context window increases latency, raises API costs, and degrades the model's reasoning capabilities due to the "needle in a haystack" problem.

In this technical guide, we will explore how to wire the Model Context Protocol's "application-controlled" primitives into a model-controlled tool-calling loop, shifting the responsibility of context retrieval from the application to the model itself.


The MCP Control Dilemma: Application-Controlled vs. Model-Controlled

The Model Context Protocol defines three primary primitives that a server can expose to a client:

  1. Tools: Executable functions that the model can choose to call.
  2. Resources: Read-only data sources (such as files, API responses, or database schemas).
  3. Prompts: Pre-defined templates or slash-commands that assist the user in formatting queries.

These primitives map differently to the execution flow of an autonomous agent:

PrimitiveWho Decides When It Is Used?Natural Fit for Tool-Calling?
ToolsThe LLM (via function-calling blocks)✅ Yes — Built natively into the reasoning loop
ResourcesThe Application or User (pre-loaded)❌ No — Traditionally stuffed into the system prompt
PromptsThe User (via UI slash-commands)❌ No — Loaded before the execution loop starts

Because resources and prompts lack a native hook in the model's autonomous reasoning loop, most developers fall back on the path of least resistance: fetching every resource at startup and dumping its full text content directly into the system prompt.

This brute-force approach introduces four major bottlenecks:

  1. Context Bloat: Every API request pays the token cost for every resource, whether the model needs it or not. Ten markdown files of 5,000 tokens each add 50,000 tokens to every single turn in the conversation.
  2. Arbitrary Truncation: To prevent runaway token costs, developers often cap resource content (e.g., content[:2000]). This silently chops off critical data at the end of large files, leading to hallucinated answers.
  3. Binary File Failures: PDF, PNG, or ZIP resources do not have plain-text representations. Naive text extractors fail with UnicodeDecodeError or output useless placeholders like [No content available].
  4. Loss of Agency: The model cannot choose what to read. It is force-fed information, preventing it from executing clean, multi-step search and retrieval strategies.

The Solution: Synthesizing Client-Side Tools

Instead of stuffing resources into the system prompt, we can translate application-controlled resources and prompts into model-controlled tools. By registering two synthetic, client-side tools—read_resource(uri) and invoke_prompt(name)—we empower the model to fetch data only when it determines that data is necessary.

Under this architecture, the system prompt does not contain any resource content. Instead, it contains a lightweight Resource Catalog listing the available URIs and their descriptions.

Here is how the system prompt changes:

## Available Resources
The following resources can be read on demand. To read one, call the
`read_resource` tool with its exact URI. Do not assume a resource's
contents until you have read it.

### Resource: SUM ABAP Test Matrix
URI: sap-btp://sum-abap-v1
Description: SUM (Software Update Manager) test matrix specification for ABAP products

When the model processes a user query, it checks the catalog. If it needs the ABAP test matrix, it emits a tool call to read_resource(uri="sap-btp://sum-abap-v1"). The orchestration graph intercepts this call, retrieves the cached content, and returns it to the model as a tool response message. The content enters the context window only for that specific turn.


Step-by-Step Implementation in Python

We will implement this pattern using LangGraph and langchain-mcp-adapters, though the architecture can be adapted to any framework.

Step 1: Define the Synthetic Tools

We define read_resource as a StructuredTool with a schema that expects a URI string. The execution function (func) is a placeholder lambda because we will intercept the tool call inside the execution graph before it hits any external server.

from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

class ReadResourceArgs(BaseModel):
    uri: str = Field(
        description="The exact URI of the MCP resource to read, e.g. 'sap-btp://sum-abap-v1'."
    )

read_resource_tool = StructuredTool.from_function(
    func=lambda uri: "",  # Placeholder lambda, intercepted in the graph execution
    name="read_resource",
    description=(
        "Read the full contents of an MCP resource by its URI. "
        "Call this when the user asks to read, open, or summarize a resource, "
        "or when you need a resource's contents to answer. "
        "Only resources listed under 'Available Resources' can be read."
    ),
    args_schema=ReadResourceArgs,
)

# Register the tool and add it to our auto-approve list
tools = [read_resource_tool]
allowed_tools_without_review = ["read_resource"]

Pro Tip: The tool's description acts as the routing prompt. Write it clearly so the LLM understands when to call it versus when to rely on its parametric knowledge.

Step 2: Cache and Index MCP Resources

To avoid network latency during the tool-calling loop, we fetch the resource metadata once when the client initializes, storing the contents in an in-memory dictionary keyed by URI.

# In-memory store for resource contents
resources_content = {}

def initialize_resource_cache(mcp_resources):
    for res in (mcp_resources or []):
        uri = getattr(res, "uri", None)
        name = getattr(res, "name", "")
        description = getattr(res, "description", "")
        content = getattr(res, "content", "")
        
        # Critical Gotcha: MCP URIs can arrive as Pydantic AnyUrl objects.
        # Convert them to plain strings to prevent dictionary lookup misses.
        uri_str = str(uri) if uri is not None else ""
        if uri_str and uri_str != "unknown":
            resources_content[uri_str] = {
                "name": name,
                "description": description,
                "content": content
            }

Step 3: Handle Binary and Non-Text Resources

If a resource is a PDF, an image, or a binary file, stuffing raw bytes into the tool output will crash the model or bloat the context window with gibberish. We parse the MIME type and return an honest descriptor for binary assets:

def is_text_mime(mime: str) -> bool:
    mime = (mime or "").lower().split(";")[0].strip()
    return (
        mime.startswith("text/")
        or mime.endswith(("+json", "+xml", "+yaml"))
        or mime in {"application/json", "application/xml", "application/yaml"}
    )

def extract_resource_payload(uri_str, resource_data, mime_type):
    if isinstance(resource_data, bytes):
        if is_text_mime(mime_type):
            return resource_data.decode("utf-8")
        else:
            # Return a clean descriptor instead of failing or sending raw binary bytes
            size_kb = len(resource_data) / 1024
            return (
                f"[Binary resource: {mime_type or 'application/octet-stream'}, "
                f"{size_kb:.2f} KB. This is not text and cannot be inlined; "
                f"open it with a client that handles its media type.]"
            )
    return str(resource_data)

Step 4: Intercept Tool Calls in the Graph

When executing the LangGraph agent, we intercept calls to read_resource before they are routed to standard tool execution nodes. This allows us to resolve the tool call instantly using our local cache.

def handle_tool_calls(state):
    messages = state["messages"]
    last_message = messages[-1]
    new_messages = []
    
    if not last_message.tool_calls:
        return {"messages": []}
        
    for tool_call in last_message.tool_calls:
        if tool_call["name"] == "read_resource":
            uri = str(tool_call["args"].get("uri", ""))
            resource = resources_content.get(uri)
            
            if resource:
                # Resolve resource content with correct MIME handling
                raw_content = resource["content"]
                mime = resource.get("mime_type", "text/plain")
                resolved_text = extract_resource_payload(uri, raw_content, mime)
                
                new_messages.append({
                    "role": "tool",
                    "name": "read_resource",
                    "content": resolved_text,
                    "tool_call_id": tool_call["id"],
                })
            else: # Handle missing resources gracefully
                available_uris = list(resources_content.keys())
                new_messages.append({
                    "role": "tool",
                    "name": "read_resource",
                    "content": f"Resource '{uri}' not found. Available URIs: {available_uris}",
                    "tool_call_id": tool_call["id"],
                })
    
    return {"messages": new_messages}

Architectural Flow

Here is how the data flows through this system during a user interaction:

   ┌─────────────────┐
User Message   └────────┬────────┘
   ┌──────────────────────────────────────────┐
System Prompt = Resource Catalog Only    (URIs & Descriptions, NO Content)   └────────┬─────────────────────────────────┘
      ┌───────────────┐
LLM Decides      └──┬─────────┬──┘
         │         │
 Needs a │         │ Doesn't Need
 Resource│         └──────────────► Answer Directly
 ┌──────────────────────────┐
 │ tool_use: read_resource  │
         (uri) └────────────┬─────────────┘
 ┌──────────────────────────────┐
Graph Intercepts the Call  (Auto-approved, no HITL gate) └────────────┬─────────────────┘
 ┌──────────────────────────────┐
Look up URI in Content Map └───────┬───────────────┬──────┘
TextBinary
         ▼               ▼
 ┌────────────────┐  ┌──────────────────────┐
Full Content   │  │ Descriptor:as Tool Message│MIME Type & Size └───────┬────────┘  └───────────┬──────────┘
         │                       │
         └───────────┬───────────┘
                     
              (Back to LLM ──► Generate Answer)

Production Gotchas and Best Practices

When deploying this pattern at scale on platforms like n1n.ai, keep these five engineering considerations in mind:

  1. Type Normalization: Ensure that you convert all URI keys in your cache to strings. Some MCP clients parse URIs as Pydantic AnyUrl objects, which will not match a standard Python string key in a dictionary lookup.
  2. Auto-Approve Read Actions: If your agent architecture uses Human-in-the-Loop (HITL) verification for tool execution, exclude read-only operations like read_resource from the verification gate. Forcing users to approve a read action breaks the user experience.
  3. MIME Location: In langchain-mcp-adapters, the MIME type resides directly on the Blob object's .mimetype attribute, not inside its metadata dictionary. Accessing the wrong property will default your files to binary octet-streams.
  4. Context Window Recycling: When using models via n1n.ai, leverage prompt caching where available. By keeping the Resource Catalog static in the system prompt, you maximize cache hit rates across conversational turns.
  5. Dynamic Pruning: If your catalog grows to hundreds of resources, do not list them all. Implement a lightweight vector search (RAG) step before generating the system prompt to select the top 10 most relevant resource descriptions and URIs.

Conclusion

By converting Model Context Protocol resources into synthetic, model-controlled tools, we transition from a wasteful "push" model of context management to an efficient "pull" model. The LLM gains agency, token consumption drops, and long-context truncation errors are eliminated.

Get a free API key at n1n.ai