cd /news/developer-tools/mcp-series-05-resources-and-prompts-… · home topics developer-tools article
[ARTICLE · art-56778] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

MCP Series (05): Resources and Prompts Deep Dive — Dynamic Data, Parameterized URIs, and Multi-Turn Templates

A developer detailed the distinction between Tools and Resources in the Model Context Protocol (MCP), explaining that Resources are read-only data sources injected by the host while Tools are actions executed by the LLM. The post demonstrated dynamic Resources that return live data on each read, parameterized URIs for handling multiple instances, and URI design principles for hierarchical organization.

read5 min views1 publishedJul 13, 2026

The split:

Tools      → actions the LLM executes (verbs)
             LLM decides when to call; calls may have side effects
             Examples: create_issue, update_status

Resources  → data the LLM reads (nouns)
             Host decides when to inject; read-only, no side effects
             Examples: current Sprint status, project statistics

The rule: "reading a state" → Resource. "Executing an operation" → Tool. The same data can have both: get_issue

as a Tool (LLM controls when to call it), jira://issue/PROJ-101

as a Resource (Host injects automatically when relevant).

A static Resource returns the same data every time (like a project list). A dynamic Resource returns the current state on each read — content changes as the underlying data changes.

Sprint status: every read returns live data

_sprint_progress_pct = 65

@server.read_resource()
async def read_resource(uri: str) -> str:
    if str(uri) == "jira://sprint/current":
        global _sprint_progress_pct
        _sprint_progress_pct = min(100, _sprint_progress_pct + random.randint(0, 3))

        return json.dumps({
            "sprint_name": "Sprint 42",
            "progress_pct": _sprint_progress_pct,                    # ← different each time
            "last_updated": datetime.now(timezone.utc).isoformat(),  # ← timestamp changes
            "days_remaining": 5,
            "p0_open": count_p0_open(),                              # ← tracks live state
        }, indent=2)

Test output:

Read 1: progress=65%  last_updated=...62+00:00
Read 2: progress=67%  last_updated=...04+00:00
→ ✓ data changed between reads

Hardcoding sprint progress in a Prompt means the LLM works from a stale snapshot. A Dynamic Resource gives it the current number on every read.

Mark the Resource as dynamic in its description so the LLM knows to re-read when it needs fresh data:

Resource(
    uri="jira://sprint/current",
    description=(
        "Live status of the active sprint: progress, issue counts. "
        "Read when the user asks about sprint health. "
        "Re-read if you need up-to-date data — content changes over time."
    ),
)

When one Resource type has many instances, use parameterized URIs. list_resources()

enumerates all instances; read_resource()

uses a single handler for all of them.

One stats Resource per project:

@server.list_resources()
async def list_resources() -> list[Resource]:
    resources = []
    for key, proj in PROJECTS.items():
        resources.append(Resource(
            uri=f"jira://project/{key}/stats",
            name=f"{proj['name']} Stats",
            description=f"Issue statistics for {proj['name']} ({key}).",
        ))
    return resources

@server.read_resource()
async def read_resource(uri: str) -> str:
    if str(uri).startswith("jira://project/") and str(uri).endswith("/stats"):
        proj_key = str(uri).split("/")[3].upper()  # parse from jira://project/{key}/stats

        if proj_key not in PROJECTS:
            raise ValueError(f"Unknown project: {proj_key}")

        proj_issues = [i for i in ISSUES.values() if i["project"] == proj_key]
        return json.dumps({
            "project": proj_key,
            "total": len(proj_issues),
            "by_status": count_by(proj_issues, "status"),
            "by_priority": count_by(proj_issues, "priority"),
        }, indent=2)

Test output:

jira://project/PROJ/stats   → total=3, by_status={'Open': 2, 'In Progress': 1}
jira://project/MOBILE/stats → total=1, by_status={'Open': 1}
jira://project/INFRA/stats  → total=1, by_status={'Done': 1}

The LLM reads only the project it needs. The Host can also inject the right Resource based on current context — if the user is working in the MOBILE project, inject MOBILE/stats

rather than dumping all projects at once.

URI design principles:

jira://project/{key}/stats    ← hierarchical path (like REST)
jira://sprint/current         ← active instance, no ID needed
jira://dashboard              ← aggregate view, fixed URI

Avoid:
jira://stats_PROJ             ← flat, doesn't scale
jira://data?project=PROJ      ← query params, harder to parse

A Prompt template doesn't have to be static text. Render different sections based on argument values so one Prompt covers multiple scenarios cleanly.

Incident report: P0 includes Escalation section, P1 doesn't

if name == "incident_report":
    severity = args.get("severity", "P1").upper()
    workaround = args.get("workaround", "")

    p0_section = ""
    if severity == "P0":
        p0_section = (
            "\n## Escalation\n"
            "- Engineering VP: notify within 30 minutes\n"
            "- SLA breach risk: may breach the 4-hour P0 SLA\n"
        )

    workaround_section = ""
    if workaround:
        workaround_section = f"\n## Workaround\n{workaround}\n"

    template = (
        f"Create a formal incident report for {issue_key}...\n"
        f"## Summary\n...\n"
        f"## Root Cause\n..."
        f"{p0_section}"          # ← conditional insert
        f"{workaround_section}"  # ← conditional insert
    )

Test output:

P0: escalation_section=✓  workaround_section=✗
P1: escalation_section=✗  workaround_section=✓

P0 incidents trigger escalation protocol; P1 incidents show the workaround. The LLM receives a different template and generates a structurally different report. No need for a single large template that the LLM has to interpret.

Standard Prompts have one user message. Multi-turn Prompts pre-fill a conversation history to guide the LLM through specific steps before producing the final output.

PR description: 3 turns, second turn is an assistant 'thinking' step

if name == "pr_description":
    return GetPromptResult(
        messages=[
            PromptMessage(role="user", content=TextContent(type="text", text=(
                f"You are a senior engineer writing a PR description.\n"
                f"PR addresses: {issue_key}\n\n"
                f"First, use get_issue to read the Jira issue details."
            ))),
            PromptMessage(role="assistant", content=TextContent(type="text", text=(
                "I'll fetch the issue details and then write a PR description "
                "with: title, motivation, changes summary, test plan, and links."
            ))),
            PromptMessage(role="user", content=TextContent(type="text", text=(
                "Now write the complete PR description in Markdown."
            ))),
        ]
    )

Test output:

Turn count: 3
Turn 1 (user):      You are a senior engineer writing a PR description...
Turn 2 (assistant): I'll fetch the issue details and then write a PR description...
Turn 3 (user):      Now write the complete PR description in Markdown.

Uses for multi-turn Prompts:

When a Prompt template embeds instructions to "read this Resource" or "call this Tool," the user doesn't need to supply data — the template tells the LLM how to get it.

if name == "standup_update":
    return GetPromptResult(messages=[PromptMessage(role="user",
        content=TextContent(type="text", text=(
            f"Generate a daily standup update for {team_member}.\n\n"
            f"Steps:\n"
            f"1. Read jira://sprint/current to see overall sprint health\n"  # ← Resource ref
            f"2. Use search_issues to find issues {team_member} worked on\n"  # ← Tool ref
            f"3. Write standup: Yesterday / Today / Blockers / Sprint health"
        ))
    )])

Test output:

References resource: ✓  (jira://sprint/current in template)
References tool:     ✓  (search_issues in template)

The LLM receives this Prompt and automatically reads jira://sprint/current

, calls search_issues

, then generates the standup. The user only needs to say "generate my standup" — no manual data gathering required.

Resources and Tools aren't mutually exclusive. A Prompt can reference both:

LLM executing standup_update:

1. Read Resource  jira://sprint/current → overall sprint health
2. Call Tool      search_issues(query="closed", assignee="alice") → completed yesterday
3. Call Tool      search_issues(query="open", assignee="alice") → planned today
4. Generate       combine data into standup format

Resources inject background context (passive, no side effects). Tools execute queries or operations (active, LLM-initiated, may have side effects). Each type handles what it's designed for.

Resources

Prompts

Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.

Find more useful knowledge and interesting products on my Homepage

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-series-05-resour…] indexed:0 read:5min 2026-07-13 ·