DEV Community

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

Posted on

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

Resources vs Tools

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
Enter fullscreen mode Exit fullscreen mode

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).


Pattern 1: Dynamic Resources

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)
Enter fullscreen mode Exit fullscreen mode

Test output:

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

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."
        # ↑ explicit signal that this is dynamic
    ),
)
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Parameterized URIs

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)
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Conditional Prompts

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", "")

    # conditional section: P0 only
    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"
        )

    # conditional section: only when workaround provided
    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
    )
Enter fullscreen mode Exit fullscreen mode

Test output:

P0: escalation_section=✓  workaround_section=✗
P1: escalation_section=✗  workaround_section=✓
Enter fullscreen mode Exit fullscreen mode

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.


Pattern 4: Multi-Turn Prompts

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=[
            # Turn 1: user sets context
            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."
            ))),
            # Turn 2: pre-filled assistant thinking step
            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."
            ))),
            # Turn 3: final instruction
            PromptMessage(role="user", content=TextContent(type="text", text=(
                "Now write the complete PR description in Markdown."
            ))),
        ]
    )
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Uses for multi-turn Prompts:

  • Pre-fill assistant thinking: plant a structured outline, guide the format of the LLM's output
  • Simulate few-shot: pre-fill example exchanges so the LLM understands the expected style
  • Chained tasks: first turn collects data, second turn processes it

Pattern 5: No-Required-Args 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"
        ))
    )])
Enter fullscreen mode Exit fullscreen mode

Test output:

References resource: ✓  (jira://sprint/current in template)
References tool:     ✓  (search_issues in template)
Enter fullscreen mode Exit fullscreen mode

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 Composing

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
Enter fullscreen mode Exit fullscreen mode

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.


Design Checklist

Resources

  • [ ] Dynamic Resources say "content changes over time" in the description
  • [ ] Parameterized URIs use hierarchical paths, not query parameters
  • [ ] Aggregate Resources (dashboard) reduce multi-tool-call round trips for overview queries

Prompts

  • [ ] Conditional sections use Python string concatenation, not empty placeholders
  • [ ] Multi-turn assistant messages contain only a thinking outline, not actual conclusions
  • [ ] No-required-args Prompts explicitly name which Resources and Tools the LLM should use

References


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

Top comments (0)